fix: stabilize Runtime workflows for 0.12.3

OpenCode status checks could cold-start throwaway servers, unrelated
conversations could block each other, and model, Continue, or remote paths
could replay or duplicate work under failure and backlog conditions. Settings
could also lose edits or overwrite unrelated model fields.

This update makes status checks passive, keeps per-conversation ordering while
allowing concurrency, preserves native long-running work with bounded display
capture and backpressure, narrows retries to known pre-dispatch failures, and
aligns remote Ask permissions without reducing Execute. It also keeps
Embedding updates narrow, improves settings and browser guidance, removes the
unused legacy Main gateway state, and records the validated Desktop 0.12.3 and
Agent 0.11.17 release metadata.

Release note: 改善 OpenCode 切换、多会话长任务、模型重试、远程 Ask/Execute 与设置保存可靠性,并完成 Desktop 0.12.3 / Agent 0.11.17 发布准备。
This commit is contained in:
mesalogo
2026-09-04 08:05:29 +08:00
parent 374968d50b
commit 4490e5fd15
60 changed files with 1639 additions and 3839 deletions
+33 -1
View File
@@ -174,7 +174,7 @@ describe('project execution space contracts', () => {
})
describe('conversation activity contracts', () => {
it('accepts more than 100 tool and subagent activities', () => {
it('accepts long activity runs within the retained display bounds', () => {
const tools = Array.from({ length: 101 }, (_, index) => ({
callId: `call-${index + 1}`,
name: 'read',
@@ -203,6 +203,7 @@ describe('conversation activity contracts', () => {
})),
createdAt: 1,
state: 'complete',
displayCaptureTruncated: true,
tools,
subagents
})
@@ -217,6 +218,37 @@ describe('conversation activity contracts', () => {
])
})
})
it('rejects conversation activity and block arrays beyond display bounds', () => {
const baseMessage = {
id: '00000000-0000-4000-8000-000000000999',
role: 'assistant' as const,
content: '',
createdAt: 1,
state: 'complete' as const
}
expect(
conversationMessageSchema.safeParse({
...baseMessage,
tools: Array.from({ length: 501 }, (_, index) => ({
callId: `call-${index + 1}`,
name: 'read',
state: 'completed' as const,
summary: `Tool ${index + 1}`
}))
}).success
).toBe(false)
expect(
conversationMessageSchema.safeParse({
...baseMessage,
blocks: Array.from({ length: 2_001 }, (_, index) => ({
id: `10000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}`,
type: 'text' as const,
content: 'x'
}))
}).success
).toBe(false)
})
})
describe('conversation branch contracts', () => {
+15 -5
View File
@@ -182,9 +182,12 @@ export const conversationMessageBlockSchema = z.discriminatedUnion('type', [
.strict()
])
export const conversationMessageBlocksSchema = z.array(
conversationMessageBlockSchema
)
export const maximumConversationRetainedActivities = 500
export const maximumConversationMessageBlocks = 2_000
export const conversationMessageBlocksSchema = z
.array(conversationMessageBlockSchema)
.max(maximumConversationMessageBlocks)
export type ConversationToolActivity = z.infer<
typeof conversationToolActivitySchema
@@ -265,6 +268,7 @@ export const conversationMessageSchema = z
content: z.string().max(1_000_000),
reasoning: z.string().optional(),
blocks: conversationMessageBlocksSchema.optional(),
displayCaptureTruncated: z.boolean().optional(),
createdAt: z.number().int().nonnegative(),
state: z.enum(['streaming', 'complete', 'error']),
status: z.string().max(4_000).optional(),
@@ -274,8 +278,14 @@ export const conversationMessageSchema = z
.array(conversationContextCompressionMarkerSchema)
.max(2)
.optional(),
tools: z.array(conversationToolActivitySchema).optional(),
subagents: z.array(conversationSubagentActivitySchema).optional(),
tools: z
.array(conversationToolActivitySchema)
.max(maximumConversationRetainedActivities)
.optional(),
subagents: z
.array(conversationSubagentActivitySchema)
.max(maximumConversationRetainedActivities)
.optional(),
sources: z.array(z.string().max(8_192)).max(100).optional(),
sourceReferences: z
.array(
-1
View File
@@ -607,7 +607,6 @@ export {
MODEL_REQUEST_CUSTOMIZATION_LIMITS,
modelRequestBodySchema,
modelRequestHeadersSchema,
canonicalModelRequestHeaders,
mergeModelRequestBody,
mergeModelRequestHeaders,
type ModelRequestBody,
@@ -1,135 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
completeModelCallEvidenceSchema,
failModelCallEvidenceSchema,
modelCallDispatchMetadataSchema,
modelCallRecordSchema,
prepareModelCallSchema
} from './model-call-operation-contracts'
const identity = {
callOperationId: 'call-1',
requestId: 'request-1',
bindingId: 'binding-1',
promptOperationId: 'prompt-1',
promptSequence: 0,
roundIndex: 0,
provider: 'provider',
profile: 'default',
model: 'model',
protocol: 'responses-v1'
}
const policy = {
modelProfileDigest: `sha256:${'b'.repeat(64)}`
}
describe('model call operation contracts', () => {
it('rejects secret or payload fields at every persistence boundary', () => {
const digest = `sha256:${'a'.repeat(64)}`
expect(() =>
prepareModelCallSchema.parse({
identity,
requestDigest: digest,
...policy,
prompt: 'secret prompt'
})
).toThrow()
expect(() =>
prepareModelCallSchema.parse({
identity: { ...identity, apiKey: 'secret-key' },
requestDigest: digest,
...policy
})
).toThrow()
expect(() =>
modelCallDispatchMetadataSchema.parse({
providerIdempotencyKey: 'safe-id',
requestBody: { messages: [] }
})
).toThrow()
expect(() =>
completeModelCallEvidenceSchema.parse({
status: 'completed',
providerResponseId: 'response-1',
responseBody: 'secret response'
})
).toThrow()
expect(() =>
failModelCallEvidenceSchema.parse({
status: 'failed-definitive',
error: {
code: 'authentication',
retryable: false,
message: 'credential was secret-value'
}
})
).toThrow()
})
it('bounds provider identifiers and safe terminal metadata', () => {
expect(() =>
modelCallDispatchMetadataSchema.parse({
providerRequestId: 'x'.repeat(257)
})
).toThrow()
expect(() =>
completeModelCallEvidenceSchema.parse({
status: 'completed',
result: { finishReason: 'x'.repeat(129) }
})
).toThrow()
})
it('tags terminal evidence and enforces record state consistency', () => {
const base = {
identity,
requestDigest: `sha256:${'a'.repeat(64)}`,
...policy,
preparedAt: 1,
updatedAt: 3,
dispatchedAt: 2,
dispatchMetadata: {}
}
expect(() =>
modelCallRecordSchema.parse({
...base,
status: 'completed',
terminalAt: 3,
terminalEvidence: {
status: 'failed-definitive',
error: { code: 'authentication', retryable: false }
}
})
).toThrow()
expect(() =>
modelCallRecordSchema.parse({
...base,
status: 'prepared'
})
).toThrow()
expect(() =>
modelCallRecordSchema.parse({
...base,
status: 'dispatched',
updatedAt: 1
})
).toThrow()
})
it('rejects prompt-wide quota fields', () => {
const request = {
identity,
requestDigest: `sha256:${'a'.repeat(64)}`,
...policy
}
expect(prepareModelCallSchema.parse(request)).toEqual(request)
expect(() =>
prepareModelCallSchema.parse({
...request,
maximumOutputTokens: 65_537,
maximumModelCalls: 100_000,
maximumTotalOutputTokens: 1_000_000_000
})
).toThrow()
})
})
@@ -1,315 +0,0 @@
import { z } from 'zod'
import {
agentIdentifierSchema,
sha256DigestSchema,
utf8StringSchema
} from './agent-protocol/contracts'
const countSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
const positiveCountSchema = countSchema.positive()
const timestampSchema = countSchema
const providerLabelSchema = utf8StringSchema(256, {
minimumBytes: 1,
label: 'Model provider label'
}).refine((value) => value.trim() === value, 'Label must not have outer whitespace')
const providerIdentifierSchema = utf8StringSchema(256, {
minimumBytes: 1,
label: 'Provider identifier'
}).refine(
(value) => value.trim() === value,
'Provider identifier must not have outer whitespace'
)
export const modelCallDigestSchema = sha256DigestSchema
export const modelCallStatusSchema = z.enum([
'prepared',
'dispatched',
'completed',
'failed-definitive',
'outcome-unknown'
])
export type ModelCallStatus = z.infer<typeof modelCallStatusSchema>
export const modelCallIdentitySchema = z
.object({
callOperationId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
bindingId: agentIdentifierSchema,
promptOperationId: agentIdentifierSchema,
promptSequence: z
.number()
.int()
.nonnegative()
.max(Number.MAX_SAFE_INTEGER)
.default(0),
roundIndex: z
.number()
.int()
.nonnegative()
.max(Number.MAX_SAFE_INTEGER),
provider: providerLabelSchema,
profile: providerLabelSchema,
model: providerLabelSchema,
protocol: providerLabelSchema
})
.strict()
export type ModelCallIdentity = z.infer<typeof modelCallIdentitySchema>
export const prepareModelCallSchema = z
.object({
identity: modelCallIdentitySchema,
requestDigest: modelCallDigestSchema,
modelProfileDigest: modelCallDigestSchema
})
.strict()
export type PrepareModelCall = z.infer<typeof prepareModelCallSchema>
export const modelCallDispatchMetadataSchema = z
.object({
providerIdempotencyKey: providerIdentifierSchema.optional(),
providerRequestId: providerIdentifierSchema.optional()
})
.strict()
export type ModelCallDispatchMetadata = z.infer<
typeof modelCallDispatchMetadataSchema
>
export const modelCallUsageMetadataSchema = z
.object({
inputTokens: countSchema.optional(),
outputTokens: countSchema.optional(),
cachedInputTokens: countSchema.optional(),
reasoningTokens: countSchema.optional()
})
.strict()
export type ModelCallUsageMetadata = z.infer<
typeof modelCallUsageMetadataSchema
>
export const modelCallResultMetadataSchema = z
.object({
finishReason: agentIdentifierSchema.optional(),
outputDigest: modelCallDigestSchema.optional(),
artifactCount: countSchema.max(1_000).optional()
})
.strict()
export type ModelCallResultMetadata = z.infer<
typeof modelCallResultMetadataSchema
>
export const modelCallErrorMetadataSchema = z
.object({
code: agentIdentifierSchema,
retryable: z.boolean()
})
.strict()
export type ModelCallErrorMetadata = z.infer<
typeof modelCallErrorMetadataSchema
>
export const completeModelCallEvidenceSchema = z
.object({
status: z.literal('completed'),
providerRequestId: providerIdentifierSchema.optional(),
providerResponseId: providerIdentifierSchema.optional(),
result: modelCallResultMetadataSchema.optional(),
usage: modelCallUsageMetadataSchema.optional()
})
.strict()
export type CompleteModelCallEvidence = z.infer<
typeof completeModelCallEvidenceSchema
>
export const failModelCallEvidenceSchema = z
.object({
status: z.literal('failed-definitive'),
providerRequestId: providerIdentifierSchema.optional(),
providerResponseId: providerIdentifierSchema.optional(),
error: modelCallErrorMetadataSchema,
usage: modelCallUsageMetadataSchema.optional()
})
.strict()
export type FailModelCallEvidence = z.infer<
typeof failModelCallEvidenceSchema
>
export const unknownModelCallEvidenceSchema = z
.object({
status: z.literal('outcome-unknown'),
providerRequestId: providerIdentifierSchema.optional(),
reason: modelCallErrorMetadataSchema
})
.strict()
export type UnknownModelCallEvidence = z.infer<
typeof unknownModelCallEvidenceSchema
>
export const modelCallTerminalEvidenceSchema = z.discriminatedUnion('status', [
completeModelCallEvidenceSchema,
failModelCallEvidenceSchema,
unknownModelCallEvidenceSchema
])
export type ModelCallTerminalEvidence = z.infer<
typeof modelCallTerminalEvidenceSchema
>
export const modelCallRecordSchema = z
.object({
identity: modelCallIdentitySchema,
requestDigest: modelCallDigestSchema,
modelProfileDigest: modelCallDigestSchema,
status: modelCallStatusSchema,
dispatchMetadata: modelCallDispatchMetadataSchema.optional(),
terminalEvidence: modelCallTerminalEvidenceSchema.optional(),
preparedAt: timestampSchema,
updatedAt: timestampSchema,
dispatchedAt: timestampSchema.optional(),
terminalAt: timestampSchema.optional(),
responseDeliveredAt: timestampSchema.optional()
})
.strict()
.superRefine((record, context) => {
if (record.updatedAt < record.preparedAt) {
addRecordIssue(context, ['updatedAt'], 'updatedAt precedes preparedAt')
}
const hasDispatch =
record.dispatchMetadata !== undefined && record.dispatchedAt !== undefined
if (
(record.dispatchMetadata === undefined) !==
(record.dispatchedAt === undefined)
) {
addRecordIssue(
context,
['dispatchMetadata'],
'dispatch metadata and timestamp must appear together'
)
}
if (
record.dispatchedAt !== undefined &&
(record.dispatchedAt < record.preparedAt ||
record.updatedAt < record.dispatchedAt)
) {
addRecordIssue(
context,
['dispatchedAt'],
'dispatchedAt is outside the record timestamp range'
)
}
if (record.status === 'prepared') {
if (
hasDispatch ||
record.terminalEvidence ||
record.terminalAt !== undefined ||
record.responseDeliveredAt !== undefined
) {
addRecordIssue(
context,
['status'],
'prepared records cannot contain dispatch or terminal state'
)
}
return
}
if (!hasDispatch) {
addRecordIssue(
context,
['dispatchMetadata'],
'dispatched and terminal records require dispatch state'
)
}
if (record.status === 'dispatched') {
if (
record.terminalEvidence ||
record.terminalAt !== undefined ||
record.responseDeliveredAt !== undefined
) {
addRecordIssue(
context,
['status'],
'dispatched records cannot contain terminal state'
)
}
return
}
if (
record.terminalEvidence?.status !== record.status ||
record.terminalAt === undefined
) {
addRecordIssue(
context,
['terminalEvidence'],
'terminal evidence and timestamp must match record status'
)
}
if (
record.terminalAt !== undefined &&
(record.dispatchedAt === undefined ||
record.terminalAt < record.dispatchedAt ||
record.updatedAt < record.terminalAt)
) {
addRecordIssue(
context,
['terminalAt'],
'terminalAt is outside the record timestamp range'
)
}
if (
record.responseDeliveredAt !== undefined &&
(record.status !== 'completed' ||
record.terminalAt === undefined ||
record.responseDeliveredAt < record.terminalAt ||
record.updatedAt < record.responseDeliveredAt)
) {
addRecordIssue(
context,
['responseDeliveredAt'],
'responseDeliveredAt requires a completed provider response'
)
}
})
export type ModelCallRecord = z.infer<typeof modelCallRecordSchema>
export const modelCallOperationCursorSchema = z
.object({
updatedAt: timestampSchema,
callOperationId: agentIdentifierSchema
})
.strict()
export type ModelCallOperationCursor = z.infer<
typeof modelCallOperationCursorSchema
>
export const modelCallDeliveryFilterSchema = z.enum([
'any',
'pending-response',
'delivered'
])
export type ModelCallDeliveryFilter = z.infer<
typeof modelCallDeliveryFilterSchema
>
export const modelCallListOptionsSchema = z
.object({
statuses: z.array(modelCallStatusSchema).min(1).max(5).optional(),
delivery: modelCallDeliveryFilterSchema.default('any'),
cursor: modelCallOperationCursorSchema.optional(),
limit: positiveCountSchema.max(1_000).default(100)
})
.strict()
export type ModelCallListOptions = z.input<
typeof modelCallListOptionsSchema
>
export type ModelCallOperationPage = {
records: ModelCallRecord[]
nextCursor?: ModelCallOperationCursor
}
function addRecordIssue(
context: z.RefinementCtx,
path: Array<string | number>,
message: string
): void {
context.addIssue({ code: 'custom', path, message })
}
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
canonicalModelRequestHeaders,
mergeModelRequestBody,
mergeModelRequestHeaders,
modelRequestBodySchema,
@@ -29,6 +28,11 @@ describe('model request customization', () => {
'x-tenant': 'two'
})
).toThrow()
expect(() =>
modelRequestHeadersSchema.parse({
'x-tenant': '租户甲'
})
).toThrow()
})
it('accepts bounded JSON bodies and rejects reserved top-level fields', () => {
@@ -83,12 +87,6 @@ describe('model request customization', () => {
'content-type': 'application/json',
'x-tenant': 'custom'
})
expect(canonicalModelRequestHeaders(headers)).toEqual({
accept: 'application/json',
'content-type': 'application/json',
'x-tenant': 'custom'
})
expect(
mergeModelRequestBody(
{
+3 -13
View File
@@ -51,10 +51,10 @@ export type ModelRequestJsonValue = CanonicalJsonValue
export type ModelRequestBody = Record<string, ModelRequestJsonValue>
function hasControlCharacter(value: string): boolean {
function hasInvalidHeaderValueCharacter(value: string): boolean {
return [...value].some((character) => {
const code = character.charCodeAt(0)
return code <= 31 || code === 127
return code <= 31 || code === 127 || code > 255
})
}
@@ -93,7 +93,7 @@ function isModelRequestHeaders(value: unknown): value is ModelRequestHeaders {
typeof headerValue !== 'string' ||
headerValue.length >
MODEL_REQUEST_CUSTOMIZATION_LIMITS.maximumHeaderValueLength ||
hasControlCharacter(headerValue)
hasInvalidHeaderValueCharacter(headerValue)
) {
return false
}
@@ -227,16 +227,6 @@ export function mergeModelRequestHeaders(
return headers
}
export function canonicalModelRequestHeaders(
headers: Headers
): Record<string, string> {
return Object.fromEntries(
[...headers.entries()].sort(([left], [right]) =>
left.localeCompare(right)
)
)
}
export function mergeModelRequestBody(
customBody: ModelRequestBody,
runtimeBody: Record<string, unknown>