fix(schedule): reconcile absolute-time stack layer

This commit is contained in:
Tianyi Cui
2026-08-08 22:59:28 +08:00
parent 120b2882c0
commit 364e6fae0f
17 changed files with 113 additions and 57 deletions

View File

@@ -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/feature/2026-08-05-durable-web-schedule.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-durable-web-schedule.md
2026-08-05-durable-web-schedule.md: 9229ff33873252ffaf13b44ccd403b12fbd656d6 2026-08-05-durable-web-schedule.md: 063f27d5bae6194b172d6998f338c11a4065bbd0
2026-08-05-durable-web-schedule.zh.md: d4050ca8295c211f9454492e5203c8ed64c368c2 2026-08-05-durable-web-schedule.zh.md: b8128d8d8e26401106a66a17a63cf8bc947b914f

View File

@@ -24,6 +24,7 @@
"@deepseek-ai/dsh-paths": "workspace:^", "@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-pty": "workspace:^", "@deepseek-ai/dsh-pty": "workspace:^",
"@deepseek-ai/dsh-pty-local": "workspace:^", "@deepseek-ai/dsh-pty-local": "workspace:^",
"@deepseek-ai/dsh-time-context": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^", "@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-tmux-context": "workspace:^", "@deepseek-ai/dsh-tmux-context": "workspace:^",
"@deepseek-ai/dsh-tool-ask-user": "workspace:^", "@deepseek-ai/dsh-tool-ask-user": "workspace:^",

View File

@@ -171,8 +171,14 @@ describe.skipIf(MODE === 'record')('web e2e: durable after reminder receipt', ()
&& event.data.source.kind === 'plugin' && event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'time-context') && event.data.source.plugin === 'time-context')
if (timeReading?.type !== 'user/message') throw new Error('missing time-context reading') if (timeReading?.type !== 'user/message') throw new Error('missing time-context reading')
expect(timeReading.data.source).toEqual({ kind: 'plugin', plugin: 'time-context' })
const timeText = timeReading.data.content.find(block => block.type === 'text')?.text const timeText = timeReading.data.content.find(block => block.type === 'text')?.text
if (timeText === undefined) throw new Error('missing time-context text')
expect(timeReading.data.source).toEqual({
kind: 'plugin',
plugin: 'time-context',
form: 'snapshot',
sections: [{ name: 'time-context', text: timeText }],
})
expect(timeText).toContain(`Session time zone: ${SESSION_TIME_ZONE}.`) expect(timeText).toContain(`Session time zone: ${SESSION_TIME_ZONE}.`)
expect(timeText).toContain('Client time zone for this request: missing.') expect(timeText).toContain('Client time zone for this request: missing.')
const listed = await scaffold.ctx.apiProxy.sessions.list({ const listed = await scaffold.ctx.apiProxy.sessions.list({
@@ -201,10 +207,15 @@ describe.skipIf(MODE === 'record')('web e2e: durable after reminder receipt', ()
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-after')) onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-after'))
const group = page.locator('[role="treeitem"]').first() const group = page.locator('[role="treeitem"]').first()
await group.waitFor({ timeout: 15_000 }) await group.waitFor({ timeout: 15_000 })
if (await group.getAttribute('aria-expanded') !== 'true') { // Startup auto-selection can race the first disclosure gesture. Converge
await group.click() // on the expanded state instead of letting that later update collapse it.
} await expect.poll(async () => {
await expect.poll(() => group.getAttribute('aria-expanded'), { timeout: 5_000 }).toBe('true') if (await group.getAttribute('aria-expanded') !== 'true') {
await group.click()
await page.waitForTimeout(50)
}
return await group.getAttribute('aria-expanded')
}, { timeout: 5_000 }).toBe('true')
const session = page.locator('[role="treeitem"][aria-selected]').nth(1) const session = page.locator('[role="treeitem"][aria-selected]').nth(1)
await session.waitFor({ timeout: 10_000 }) await session.waitFor({ timeout: 10_000 })
await session.click() await session.click()
@@ -387,7 +398,7 @@ describe.skipIf(MODE === 'record')('web e2e: Schedule restart, fork, and cold hi
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, world }) scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, world })
const pendingResume = await scaffold.ctx.apiProxy.sessions.create({ const pendingResume = await scaffold.ctx.apiProxy.sessions.create({
rpcId: RpcId('schedule-pending-resume'), rpcId: RpcId('schedule-pending-resume'),
payload: { sessionId: pendingId, cwd: workspaceCwd }, payload: { sessionId: pendingId, cwd: workspaceCwd, timeZone: 'UTC' },
}) })
if (!pendingResume.result.ok) throw new Error(pendingResume.result.error.message) if (!pendingResume.result.ok) throw new Error(pendingResume.result.error.message)
const pendingAgent = scaffold.ctx.agents.get(pendingId) const pendingAgent = scaffold.ctx.agents.get(pendingId)
@@ -411,7 +422,7 @@ describe.skipIf(MODE === 'record')('web e2e: Schedule restart, fork, and cold hi
const deliveredResume = await scaffold.ctx.apiProxy.sessions.create({ const deliveredResume = await scaffold.ctx.apiProxy.sessions.create({
rpcId: RpcId('schedule-delivered-resume'), rpcId: RpcId('schedule-delivered-resume'),
payload: { sessionId: deliveredId, cwd: workspaceCwd }, payload: { sessionId: deliveredId, cwd: workspaceCwd, timeZone: 'UTC' },
}) })
if (!deliveredResume.result.ok) throw new Error(deliveredResume.result.error.message) if (!deliveredResume.result.ok) throw new Error(deliveredResume.result.error.message)
const deliveredAgent = scaffold.ctx.agents.get(deliveredId) const deliveredAgent = scaffold.ctx.agents.get(deliveredId)
@@ -446,7 +457,7 @@ describe.skipIf(MODE === 'record')('web e2e: Schedule restart, fork, and cold hi
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, world }) scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, world })
const replayed = await scaffold.ctx.apiProxy.sessions.create({ const replayed = await scaffold.ctx.apiProxy.sessions.create({
rpcId: RpcId('schedule-delivered-replay'), rpcId: RpcId('schedule-delivered-replay'),
payload: { sessionId: deliveredId, cwd: workspaceCwd }, payload: { sessionId: deliveredId, cwd: workspaceCwd, timeZone: 'UTC' },
}) })
if (!replayed.result.ok) throw new Error(replayed.result.error.message) if (!replayed.result.ok) throw new Error(replayed.result.error.message)
const replayedAgent = scaffold.ctx.agents.get(deliveredId) const replayedAgent = scaffold.ctx.agents.get(deliveredId)

View File

@@ -78,7 +78,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T] }[T]
``` ```
Sources: [`packages/core/session/src/types.ts:308`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:315`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:375`](../packages/core/session/src/types.ts) Sources: [`packages/core/session/src/types.ts:315`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:322`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:350`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:382`](../packages/core/session/src/types.ts)
## Events ## Events
@@ -175,7 +175,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/inter
Types: [StreamChunk](subsystems/llm-streaming.md) Types: [StreamChunk](subsystems/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface #### `assistant/message` — surface
@@ -191,7 +191,7 @@ Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/
Types: [TokenUsage](subsystems/llm-streaming.md) Types: [TokenUsage](subsystems/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts)
### `command/*` ### `command/*`
@@ -479,7 +479,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/s
'request/context': RequestContext 'request/context': RequestContext
``` ```
Source: [`packages/core/session/src/types.ts:281`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:288`](../packages/core/session/src/types.ts)
#### `request/header` — log-only #### `request/header` — log-only
@@ -491,7 +491,7 @@ Source: [`packages/core/session/src/types.ts:281`](../packages/core/session/src/
'request/header': { header: EpochHeader; reason: RequestHeaderReason } 'request/header': { header: EpochHeader; reason: RequestHeaderReason }
``` ```
Source: [`packages/core/session/src/types.ts:276`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts)
### `sandbox/*` ### `sandbox/*`
@@ -558,7 +558,7 @@ Source: [`packages/schedule/tool-schedule/src/types.ts:202`](../packages/schedul
'session/end-seed': Record<string, never> 'session/end-seed': Record<string, never>
``` ```
Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts)
#### `session/title` — log-only #### `session/title` — log-only
@@ -594,7 +594,7 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi
'step/end': { turn: number; step: number } 'step/end': { turn: number; step: number }
``` ```
Source: [`packages/core/session/src/types.ts:228`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:235`](../packages/core/session/src/types.ts)
#### `step/start` — log-only #### `step/start` — log-only
@@ -603,7 +603,7 @@ Source: [`packages/core/session/src/types.ts:228`](../packages/core/session/src/
'step/start': { turn: number; step: number } 'step/start': { turn: number; step: number }
``` ```
Source: [`packages/core/session/src/types.ts:226`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:233`](../packages/core/session/src/types.ts)
### `subagent/*` ### `subagent/*`
@@ -633,7 +633,7 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent
Types: [TodoItem](subsystems/session.md) Types: [TodoItem](subsystems/session.md)
Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:278`](../packages/core/session/src/types.ts)
### `tool/*` ### `tool/*`
@@ -650,7 +650,7 @@ Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/
Types: [CallId](subsystems/core.md) Types: [CallId](subsystems/core.md)
Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:258`](../packages/core/session/src/types.ts)
#### `tool/code-dispatch` — log-only #### `tool/code-dispatch` — log-only
@@ -723,7 +723,7 @@ Source: [`packages/core/tools/src/code-mode.ts:33`](../packages/core/tools/src/c
} }
``` ```
Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:270`](../packages/core/session/src/types.ts)
### `turn/*` ### `turn/*`
@@ -743,7 +743,7 @@ Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/
Types: [TurnEndReason](subsystems/session.md) Types: [TurnEndReason](subsystems/session.md)
Source: [`packages/core/session/src/types.ts:224`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:231`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only #### `turn/start` — log-only
@@ -757,7 +757,7 @@ Source: [`packages/core/session/src/types.ts:224`](../packages/core/session/src/
'turn/start': { turn: number } 'turn/start': { turn: number }
``` ```
Source: [`packages/core/session/src/types.ts:215`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:222`](../packages/core/session/src/types.ts)
### `user/*` ### `user/*`
@@ -774,7 +774,7 @@ Source: [`packages/core/session/src/types.ts:215`](../packages/core/session/src/
'user/message': UserMessage 'user/message': UserMessage
``` ```
Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts) Source: [`packages/core/session/src/types.ts:243`](../packages/core/session/src/types.ts)
### `web/*` ### `web/*`

View File

@@ -831,7 +831,7 @@ create, edit, pause, and resume require direct-human root authority; complete an
### `schedule_create` ### `schedule_create`
Create one reminder in the current session. v1 accepts only a non-empty prompt and a positive safe-integer after_seconds delay. Delivery is session-local: the reminder runs on time only while this session is live and otherwise becomes overdue until the session is resumed. Create one reminder in the current session. Supply a non-empty prompt and exactly one selector: a positive safe-integer after_seconds delay, or at as a strict offset date-time or local date/time object. Delivery is session-local: the reminder runs on time only while this session is live and otherwise becomes overdue until the session is resumed.
```json ```json
{ {
@@ -844,11 +844,37 @@ Create one reminder in the current session. v1 accepts only a non-empty prompt a
"after_seconds": { "after_seconds": {
"type": "number", "type": "number",
"description": "Positive safe-integer delay in seconds." "description": "Positive safe-integer delay in seconds."
},
"at": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"date": {
"type": "string"
},
"time": {
"type": "string"
},
"time_zone": {
"type": "string"
}
},
"required": [
"date",
"time"
]
}
],
"description": "Absolute target as strict offset RFC 3339 or local date/time with optional IANA zone."
} }
}, },
"required": [ "required": [
"prompt", "prompt"
"after_seconds"
] ]
} }
``` ```

View File

@@ -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 examples/README.md # pnpm run verify-translation-pairing --write examples/README.md
README.md: 826e15e461d2544d664ce73683031b8cc0307595 README.md: b6e91bc544111275c1dfc07067eff97fde1ceb12
README.zh.md: 97f6722f9bf16073af5605ff5c3d3efb8fddf435 README.zh.md: e8eee83446aa9e3232957d567e510a3998f39ec8

File diff suppressed because one or more lines are too long

View File

@@ -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 examples/web-schedule/README.md # pnpm run verify-translation-pairing --write examples/web-schedule/README.md
README.md: df685a5e53972eff8499f19394c0815fe434c148 README.md: 303f616b90fb7b8318c37ab5eca999cd855c23c6
README.zh.md: 849a16a72b9527a3b2ba3cc35534a05e8c6e3d9b README.zh.md: b9fc3b69c7f530f46f9d9776061114710d9f6f29

View File

@@ -810,7 +810,9 @@ describe('createFixtureApi', () => {
] as const)('rejects invalid fixture %s input %j', async (field, value) => { ] as const)('rejects invalid fixture %s input %j', async (field, value) => {
const api = createFixtureApi({ empty: true }) const api = createFixtureApi({ empty: true })
if (field === 'timeZone') { if (field === 'timeZone') {
const created = await api.sessions.create(req({ timeZone: value })) const invalidRequest = req({})
Object.assign(invalidRequest.payload, { timeZone: value })
const created = await api.sessions.create(invalidRequest)
expect(created.result).toMatchObject({ expect(created.result).toMatchObject({
ok: false, ok: false,
error: { code: 'invalid-time-zone', details: { field, value: value ?? null } }, error: { code: 'invalid-time-zone', details: { field, value: value ?? null } },
@@ -819,12 +821,13 @@ describe('createFixtureApi', () => {
} }
const created = await api.sessions.create(req({ timeZone: 'UTC' })) const created = await api.sessions.create(req({ timeZone: 'UTC' }))
if (!created.result.ok) throw new Error('fixture create failed') if (!created.result.ok) throw new Error('fixture create failed')
const prompted = await api.sessions.prompt(req({ const invalidRequest = req({
sessionId: created.result.value.sessionId, sessionId: created.result.value.sessionId,
mode: 'queue', mode: 'queue' as const,
content: [{ type: 'text', text: 'rejected' }], content: [{ type: 'text' as const, text: 'rejected' }],
clientTimeZone: value, })
})) Object.assign(invalidRequest.payload, { clientTimeZone: value })
const prompted = await api.sessions.prompt(invalidRequest)
expect(prompted.result).toMatchObject({ expect(prompted.result).toMatchObject({
ok: false, ok: false,
error: { code: 'invalid-time-zone', details: { field, value: value ?? null } }, error: { code: 'invalid-time-zone', details: { field, value: value ?? null } },

View File

@@ -237,9 +237,19 @@ describe('durable step context', () => {
const event = session.events.at(-1) const event = session.events.at(-1)
expect(event?.type).toBe('user/message') expect(event?.type).toBe('user/message')
if (event?.type !== 'user/message') throw new Error('missing time context') if (event?.type !== 'user/message') throw new Error('missing time context')
const text = event.data.content.find(block => block.type === 'text')?.text
if (text === undefined) throw new Error('missing time-context text')
// The reading is a `snapshot`-form context: one named contribution whose
// text is exactly what the model read, so a consumer attributes it without
// re-splitting prose.
expect(event.data.source).toEqual({ expect(event.data.source).toEqual({
kind: 'plugin', kind: 'plugin',
plugin: 'time-context', plugin: 'time-context',
form: 'snapshot',
sections: [{
name: 'time-context',
text,
}],
}) })
expect(event.surfaceOp).toBe('append') expect(event.surfaceOp).toBe('append')
}) })
@@ -550,15 +560,15 @@ describe('real agent-loop request history', () => {
it('does not revive an empty continuation after a completed step', async () => { it('does not revive an empty continuation after a completed step', async () => {
const adapter = new ScriptedAdapter([textResponse('done')]) const adapter = new ScriptedAdapter([textResponse('done')])
const ctx = await loopHarness(adapter) const ctx = await loopHarness(adapter)
ctx.on('agent/turn-stopping', (subject) => { ctx.on('agent/turn-stopping', ({ agent: subject }) => {
subject.inject(createUserMessage({ subject.inject(createUserMessage({
content: [{ type: 'text', text: 'pending context' }], content: [{ type: 'text', text: 'pending context' }],
source: { kind: 'plugin', plugin: 'test' }, source: { kind: 'plugin', plugin: 'test' },
})) }))
}) })
ctx.on('agent/pre-step', async (_agent, _messages, context, next) => { ctx.on('agent/pre-step', async ({ step }, next) => {
const decision = await next() const decision = await next()
return context.step === 1 || decision.kind === 'reject' return step === 1 || decision.kind === 'reject'
? decision ? decision
: { kind: 'enter', messages: [] } : { kind: 'enter', messages: [] }
}) })

View File

@@ -79,11 +79,11 @@ export interface CreateAgentOptions {
/** The live agent/session identity. */ /** The live agent/session identity. */
readonly sessionId: SessionId readonly sessionId: SessionId
/** /**
* Session creation metadata: validated absolute `cwd`, `parentSession` * Session creation metadata: validated absolute `cwd`, caller-validated
* fork lineage, the `seedLength` seed boundary, the coarse `origin` * `timeZone`, `parentSession` fork lineage, the `seedLength` seed boundary,
* classification, and the `delegationDepth` recursion budget. Mirrors the * the coarse `origin` classification, and the `delegationDepth` recursion
* `cwd`/`parentSession`/`seedLength`/`origin`/`delegationDepth` fields of * budget. Mirrors the `cwd`/`timeZone`/`parentSession`/`seedLength`/`origin`/
* {@link CreateSessionOptions.meta} in dsh-session (the internal-only * `delegationDepth` fields of {@link CreateSessionOptions.meta} in dsh-session (the internal-only
* `createdAt`, used when reconstructing a persisted session, is deliberately * `createdAt`, used when reconstructing a persisted session, is deliberately
* excluded — a factory caller never sets it). This is durable session data, * excluded — a factory caller never sets it). This is durable session data,
* so the session boundary validates and snapshots it before asynchronous * so the session boundary validates and snapshots it before asynchronous
@@ -91,6 +91,7 @@ export interface CreateAgentOptions {
*/ */
readonly meta?: { readonly meta?: {
readonly cwd?: string readonly cwd?: string
readonly timeZone?: string
readonly parentSession?: SessionId readonly parentSession?: SessionId
readonly seedLength?: number readonly seedLength?: number
readonly origin?: 'subagent' readonly origin?: 'subagent'

View File

@@ -527,7 +527,7 @@ describe('cold Session zone identity', () => {
locate: () => undefined, locate: () => undefined,
} as never) } as never)
const resume = vi.spyOn(ctx.agents, 'resume') const resume = vi.spyOn(ctx.agents, 'resume')
const api = createApiProxy(ctx, { provider: 'p', model: 'm', cwd: '/tmp', workspaceRoot: '/tmp' }) const api = createApiProxy(ctx, { defaultTarget: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', workspaceRoot: '/tmp' })
const response = await api.sessions.create(request({ const response = await api.sessions.create(request({
sessionId, sessionId,

View File

@@ -315,6 +315,7 @@ describe('Web session model selection', () => {
// callable, so the refusal has to live here. // callable, so the refusal has to live here.
const refused = await api.sessions.prompt(request({ const refused = await api.sessions.prompt(request({
sessionId, mode: 'queue' as const, content: [{ type: 'text' as const, text: 'hi' }], sessionId, mode: 'queue' as const, content: [{ type: 'text' as const, text: 'hi' }],
clientTimeZone: 'UTC',
})) }))
expect(refused.result).toMatchObject({ expect(refused.result).toMatchObject({
ok: false, ok: false,

View File

@@ -441,7 +441,9 @@ describe('session creation and Workspace membership', () => {
['Not/A_Real_Zone', 'Not/A_Real_Zone'], ['Not/A_Real_Zone', 'Not/A_Real_Zone'],
] as const)('rejects invalid Session zone input %j before Agent creation', async (timeZone, value) => { ] as const)('rejects invalid Session zone input %j before Agent creation', async (timeZone, value) => {
const { api, ctx } = await harness() const { api, ctx } = await harness()
const response = await api.sessions.create(request({ timeZone })) const invalidRequest = request({})
Object.assign(invalidRequest.payload, { timeZone })
const response = await api.sessions.create(invalidRequest)
expect(response.result).toMatchObject({ expect(response.result).toMatchObject({
ok: false, ok: false,
@@ -493,12 +495,13 @@ describe('session creation and Workspace membership', () => {
if (agent === undefined) throw new Error('created Agent missing') if (agent === undefined) throw new Error('created Agent missing')
const followup = vi.spyOn(agent, 'followup') const followup = vi.spyOn(agent, 'followup')
const response = await api.sessions.prompt(request({ const invalidRequest = request({
sessionId, sessionId,
mode: 'queue', mode: 'queue' as const,
content: [{ type: 'text', text: 'rejected' }], content: [{ type: 'text' as const, text: 'rejected' }],
clientTimeZone, })
})) Object.assign(invalidRequest.payload, { clientTimeZone })
const response = await api.sessions.prompt(invalidRequest)
expect(response.result).toMatchObject({ expect(response.result).toMatchObject({
ok: false, ok: false,

View File

@@ -59,7 +59,7 @@ describe('rpcErrorSchema', () => {
expect(rpcErrorSchema.parse({ code: 'bad-request', message: 'm', details: { issues: [] } }).code).toBe('bad-request') expect(rpcErrorSchema.parse({ code: 'bad-request', message: 'm', details: { issues: [] } }).code).toBe('bad-request')
expect(rpcErrorSchema.parse({ code: 'cancelled', message: 'm', details: {} }).code).toBe('cancelled') expect(rpcErrorSchema.parse({ code: 'cancelled', message: 'm', details: {} }).code).toBe('cancelled')
expect(rpcErrorSchema.parse({ code: 'session-not-found', message: 'm', details: { sessionId: 's' } }).code).toBe('session-not-found') expect(rpcErrorSchema.parse({ code: 'session-not-found', message: 'm', details: { sessionId: 's' } }).code).toBe('session-not-found')
expect(rpcErrorSchema.parse({ code: 'session-conflict', message: 'm', details: { sessionId: 's', requestedCwd: '/a', existingCwd: '/b' } }).code).toBe('session-conflict') expect(rpcErrorSchema.parse({ code: 'session-conflict', message: 'm', details: { sessionId: 's', requestedCwd: '/a', existingCwd: '/b', requestedTimeZone: 'UTC' } }).code).toBe('session-conflict')
expect(rpcErrorSchema.parse({ code: 'workspace-attach-failed', message: 'm', details: { sessionId: 's', workspaceId: 'w' } }).code).toBe('workspace-attach-failed') expect(rpcErrorSchema.parse({ code: 'workspace-attach-failed', message: 'm', details: { sessionId: 's', workspaceId: 'w' } }).code).toBe('workspace-attach-failed')
expect(rpcErrorSchema.parse({ code: 'workspace-not-found', message: 'm', details: { workspaceId: 'w' } }).code).toBe('workspace-not-found') expect(rpcErrorSchema.parse({ code: 'workspace-not-found', message: 'm', details: { workspaceId: 'w' } }).code).toBe('workspace-not-found')
expect(rpcErrorSchema.parse({ code: 'workspace-invalid-path', message: 'm', details: { path: '/x' } }).code).toBe('workspace-invalid-path') expect(rpcErrorSchema.parse({ code: 'workspace-invalid-path', message: 'm', details: { path: '/x' } }).code).toBe('workspace-invalid-path')

View File

@@ -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/schedule/tool-schedule/README.md # pnpm run verify-translation-pairing --write packages/schedule/tool-schedule/README.md
README.md: a0a51a94ff9b529a8c8f73e87d8ba75af50ffbc9 README.md: 3e0a0cea98dbe593974c5604736d155508f28044
README.zh.md: 0cbbe4e290c6877cd7af3e73c9a595bb992ca637 README.zh.md: b08bad14d50b07af2c36796335452b835fb9680a

View File

@@ -1909,7 +1909,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
}, },
{ {
name: 'CreateAgentOptions', name: 'CreateAgentOptions',
declaration: 'export interface CreateAgentOptions {\n readonly sessionId: SessionId;\n readonly meta?: {\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly origin?: \'subagent\';\n readonly delegationDepth?: number;\n };\n readonly seed?: readonly SessionEvent[];\n readonly agentOptions?: AgentOptions;\n readonly signal?: AbortSignal;\n readonly setup?: AgentSetup;\n}', declaration: 'export interface CreateAgentOptions {\n readonly sessionId: SessionId;\n readonly meta?: {\n readonly cwd?: string;\n readonly timeZone?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly origin?: \'subagent\';\n readonly delegationDepth?: number;\n };\n readonly seed?: readonly SessionEvent[];\n readonly agentOptions?: AgentOptions;\n readonly signal?: AbortSignal;\n readonly setup?: AgentSetup;\n}',
}, },
{ {
name: 'CreateGoalRequest', name: 'CreateGoalRequest',