mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Preserve uncertain goal text and accept distinct controls (#33022)
* fix: preserve goal control cancellation and uncertain outcomes * fix(i18n): retire obsolete goal failure translations * fix: keep goal controls independent of pending actions * test: cover unknown goal recovery and later edits * test: replay recorded Codex goal notifications through journal * test: type recorded goal projections from reader return
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{"receivedAt":"2026-10-24T06:35:28.038Z","direction":"response","frame":{"method":"thread/goal/updated","params":{"threadId":"<threadId-1>","turnId":null,"goal":{"threadId":"<threadId-1>","objective":"Inspect each of samples/sample-01.txt through sample-12.txt in order using a separate read for each, then compare inventory.txt and README.md and report a 12-row summary. No file edits or external network.","status":"active","tokenBudget":null,"tokensUsed":0,"timeUsedSeconds":0,"createdAt":1792823727,"updatedAt":1792823727}},"emittedAtMs":1792823728031}}
|
||||
{"receivedAt":"2026-10-24T06:35:28.872Z","direction":"response","frame":{"method":"thread/goal/updated","params":{"threadId":"<threadId-1>","turnId":null,"goal":{"threadId":"<threadId-1>","objective":"Inspect each of samples/sample-01.txt through sample-12.txt in order using a separate read for each, then compare inventory.txt and README.md and report a 12-row summary. No file edits or external network.","status":"paused","tokenBudget":null,"tokensUsed":0,"timeUsedSeconds":0,"createdAt":1792823727,"updatedAt":1792823728}},"emittedAtMs":1792823728871}}
|
||||
{"receivedAt":"2026-10-24T06:35:37.636Z","direction":"response","frame":{"method":"thread/goal/updated","params":{"threadId":"<threadId-1>","turnId":null,"goal":{"threadId":"<threadId-1>","objective":"Inspect each of samples/sample-01.txt through sample-12.txt in order using a separate read for each, then compare inventory.txt and README.md and report a 12-row summary. No file edits or external network.","status":"active","tokenBudget":null,"tokensUsed":0,"timeUsedSeconds":0,"createdAt":1792823727,"updatedAt":1792823737}},"emittedAtMs":1792823737636}}
|
||||
{"receivedAt":"2026-10-24T06:35:53.377Z","direction":"response","frame":{"method":"thread/goal/cleared","params":{"threadId":"<threadId-1>"},"emittedAtMs":1792823753377}}
|
||||
{"receivedAt":"2026-10-24T06:35:53.382Z","direction":"response","frame":{"method":"thread/goal/updated","params":{"threadId":"<threadId-1>","turnId":null,"goal":{"threadId":"<threadId-1>","objective":"Inspect samples/sample-12.txt through samples/sample-01.txt in reverse order using a separate read for each, compare their fruit with inventory.txt, and report a reverse-order 12-row summary. No edits or external network.","status":"active","tokenBudget":null,"tokensUsed":0,"timeUsedSeconds":0,"createdAt":1792823753,"updatedAt":1792823753}},"emittedAtMs":1792823753381}}
|
||||
{"receivedAt":"2026-10-24T06:35:53.761Z","direction":"response","frame":{"method":"thread/goal/cleared","params":{"threadId":"<threadId-1>"},"emittedAtMs":1792823753760}}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../shared/agent-session-journal-types'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -14,6 +15,7 @@ import { createTrackedJournalOpener } from '../native-chat/agent-session-journal
|
||||
import { readAgentSessionHistory } from '../native-chat/agent-session-wire/agent-session-history-page'
|
||||
import { createDeferredStructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
|
||||
import { CodexJournalGoals } from './codex-structured-journal-goals'
|
||||
import { createCodexJournalTranslator } from './codex-structured-journal-translation'
|
||||
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
|
||||
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
|
||||
|
||||
@@ -87,6 +89,72 @@ function journalSink(journal: AgentSessionJournal) {
|
||||
}
|
||||
|
||||
describe('codex goal accounting revisions', () => {
|
||||
it('projects recorded Codex 0.163.0 goal controls through the journal to the banner state', async () => {
|
||||
// Actual UI Set/Pause/Resume/Edit/Clear notifications; only provider IDs are redacted.
|
||||
const notifications: {
|
||||
receivedAt: string
|
||||
frame: { method: string; params: { threadId: string }; emittedAtMs: number }
|
||||
}[] = readFileSync(
|
||||
join(__dirname, '__fixtures__', 'codex-app-server-goal-lifecycle.jsonl'),
|
||||
'utf8'
|
||||
)
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => JSON.parse(line))
|
||||
const threadId = '<threadId-1>'
|
||||
root = await mkdtemp(join(tmpdir(), 'orca-goal-recorded-'))
|
||||
const journal = await journals.open({
|
||||
identity: { ...IDENTITY, providerHandle: codexProviderHandle(threadId) },
|
||||
stateDirectory: root
|
||||
})
|
||||
const { sink, drained } = journalSink(journal)
|
||||
const translator = createCodexJournalTranslator({
|
||||
sink,
|
||||
sessionId: IDENTITY.sessionId,
|
||||
primaryThreadId: () => threadId
|
||||
})
|
||||
const projected: ReturnType<typeof currentAgentSessionThreadGoal>[] = []
|
||||
for (const { receivedAt, frame } of notifications) {
|
||||
expect(
|
||||
translator.handle({
|
||||
type: 'notification',
|
||||
sessionId: IDENTITY.sessionId,
|
||||
threadId: frame.params.threadId,
|
||||
method: frame.method,
|
||||
params: frame.params,
|
||||
observedAt: Date.parse(receivedAt)
|
||||
})
|
||||
).toEqual({ accepted: true })
|
||||
await drained()
|
||||
projected.push(currentAgentSessionThreadGoal(journal.snapshot().items))
|
||||
}
|
||||
const original = {
|
||||
objective:
|
||||
'Inspect each of samples/sample-01.txt through sample-12.txt in order using a separate read for each, then compare inventory.txt and README.md and report a 12-row summary. No file edits or external network.',
|
||||
status: 'active',
|
||||
tokenBudget: null,
|
||||
tokensUsed: 0,
|
||||
timeUsedSeconds: 0,
|
||||
createdAt: 1792823727_000,
|
||||
updatedAt: 1792823727_000
|
||||
}
|
||||
expect(projected).toEqual([
|
||||
original,
|
||||
{ ...original, status: 'paused', updatedAt: 1792823728_000 },
|
||||
{ ...original, updatedAt: 1792823737_000 },
|
||||
null,
|
||||
{
|
||||
...original,
|
||||
objective:
|
||||
'Inspect samples/sample-12.txt through samples/sample-01.txt in reverse order using a separate read for each, compare their fruit with inventory.txt, and report a reverse-order 12-row summary. No edits or external network.',
|
||||
createdAt: 1792823753_000,
|
||||
updatedAt: 1792823753_000
|
||||
},
|
||||
null
|
||||
])
|
||||
translator.dispose()
|
||||
})
|
||||
|
||||
it('revises the goal row in place: one visible row, pinned sequence, fresh counters', async () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'orca-goal-revision-'))
|
||||
const journal = await journals.open({ identity: IDENTITY, stateDirectory: root })
|
||||
|
||||
@@ -18,33 +18,33 @@ describe('codex thread goal requests', () => {
|
||||
expect(codexThreadGoalRequests(THREAD, { kind: 'set', objective: 'Ship it' }, false)).toEqual([
|
||||
{
|
||||
method: 'thread/goal/set',
|
||||
params: { threadId: THREAD, objective: 'Ship it', status: 'active' }
|
||||
params: { threadId: THREAD, origin: 'user', objective: 'Ship it', status: 'active' }
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('clears the existing goal before setting a new objective, so the new goal starts fresh', () => {
|
||||
expect(codexThreadGoalRequests(THREAD, { kind: 'set', objective: 'Ship it' }, true)).toEqual([
|
||||
{ method: 'thread/goal/clear', params: { threadId: THREAD } },
|
||||
{ method: 'thread/goal/clear', params: { threadId: THREAD, origin: 'user' } },
|
||||
{
|
||||
method: 'thread/goal/set',
|
||||
params: { threadId: THREAD, objective: 'Ship it', status: 'active' }
|
||||
params: { threadId: THREAD, origin: 'user', objective: 'Ship it', status: 'active' }
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('pauses and resumes by status alone, keeping the objective', () => {
|
||||
expect(codexThreadGoalRequests(THREAD, { kind: 'status', status: 'paused' }, true)).toEqual([
|
||||
{ method: 'thread/goal/set', params: { threadId: THREAD, status: 'paused' } }
|
||||
{ method: 'thread/goal/set', params: { threadId: THREAD, origin: 'user', status: 'paused' } }
|
||||
])
|
||||
expect(codexThreadGoalRequests(THREAD, { kind: 'status', status: 'active' }, true)).toEqual([
|
||||
{ method: 'thread/goal/set', params: { threadId: THREAD, status: 'active' } }
|
||||
{ method: 'thread/goal/set', params: { threadId: THREAD, origin: 'user', status: 'active' } }
|
||||
])
|
||||
})
|
||||
|
||||
it('clears with only the thread id', () => {
|
||||
it('clears with user provenance', () => {
|
||||
expect(codexThreadGoalRequests(THREAD, { kind: 'clear' }, true)).toEqual([
|
||||
{ method: 'thread/goal/clear', params: { threadId: THREAD } }
|
||||
{ method: 'thread/goal/clear', params: { threadId: THREAD, origin: 'user' } }
|
||||
])
|
||||
})
|
||||
|
||||
@@ -54,10 +54,10 @@ describe('codex thread goal requests', () => {
|
||||
changeCodexThreadGoal(session(request), { kind: 'set', objective: 'Ship it' }, true, 5_000)
|
||||
).resolves.toEqual({ ok: true })
|
||||
expect(request.mock.calls).toEqual([
|
||||
['thread/goal/clear', { threadId: THREAD }, { timeoutMs: 5_000 }],
|
||||
['thread/goal/clear', { threadId: THREAD, origin: 'user' }, { timeoutMs: 5_000 }],
|
||||
[
|
||||
'thread/goal/set',
|
||||
{ threadId: THREAD, objective: 'Ship it', status: 'active' },
|
||||
{ threadId: THREAD, origin: 'user', objective: 'Ship it', status: 'active' },
|
||||
{ timeoutMs: 5_000 }
|
||||
]
|
||||
])
|
||||
@@ -84,6 +84,27 @@ describe('codex thread goal requests', () => {
|
||||
expect(request).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('reports a cleared previous goal when replacement is refused', async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ cleared: true })
|
||||
.mockRejectedValueOnce(
|
||||
new CodexAppServerRequestError('thread/goal/set', -32600, 'goals disabled')
|
||||
)
|
||||
await expect(
|
||||
changeCodexThreadGoal(
|
||||
session(request),
|
||||
{ kind: 'set', objective: 'Ship it' },
|
||||
true,
|
||||
undefined
|
||||
)
|
||||
).resolves.toEqual({
|
||||
ok: false,
|
||||
rejected:
|
||||
'Codex cleared the previous goal but could not set the new goal. Set the goal again to continue.'
|
||||
})
|
||||
})
|
||||
|
||||
it('stops at a refused clear, so a refused replacement leaves the old goal alone', async () => {
|
||||
const request = vi.fn(async (method: unknown) => {
|
||||
if (method === 'thread/goal/clear') {
|
||||
|
||||
@@ -13,19 +13,24 @@ export function codexThreadGoalRequests(
|
||||
change: AgentSessionThreadGoalChange,
|
||||
replacesGoal: boolean
|
||||
): CodexThreadGoalRequest[] {
|
||||
const clear: CodexThreadGoalRequest = { method: 'thread/goal/clear', params: { threadId } }
|
||||
const clear: CodexThreadGoalRequest = {
|
||||
method: 'thread/goal/clear',
|
||||
params: { threadId, origin: 'user' }
|
||||
}
|
||||
if (change.kind === 'clear') {
|
||||
return [clear]
|
||||
}
|
||||
if (change.kind === 'status') {
|
||||
return [{ method: 'thread/goal/set', params: { threadId, status: change.status } }]
|
||||
return [
|
||||
{ method: 'thread/goal/set', params: { threadId, origin: 'user', status: change.status } }
|
||||
]
|
||||
}
|
||||
// Clear replacements to reset identity/usage; activating a goal starts work without a turn.
|
||||
return [
|
||||
...(replacesGoal ? [clear] : []),
|
||||
{
|
||||
method: 'thread/goal/set',
|
||||
params: { threadId, objective: change.objective, status: 'active' }
|
||||
params: { threadId, origin: 'user', objective: change.objective, status: 'active' }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -37,16 +42,23 @@ export async function changeCodexThreadGoal(
|
||||
timeoutMs: number | undefined,
|
||||
signal?: AbortSignal
|
||||
): Promise<{ ok: true } | { ok: false; rejected: string }> {
|
||||
let cleared = false
|
||||
try {
|
||||
for (const request of codexThreadGoalRequests(session.threadId, change, replacesGoal)) {
|
||||
throwIfSignalAborted(signal)
|
||||
await session.connection.request(request.method, request.params, { timeoutMs })
|
||||
cleared ||= request.method === 'thread/goal/clear'
|
||||
}
|
||||
return { ok: true }
|
||||
} catch (error) {
|
||||
if (isCodexAppServerRequestError(error)) {
|
||||
return { ok: false, rejected: error.message }
|
||||
return {
|
||||
ok: false,
|
||||
rejected: cleared
|
||||
? 'Codex cleared the previous goal but could not set the new goal. Set the goal again to continue.'
|
||||
: error.message
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
+103
-31
@@ -1,10 +1,20 @@
|
||||
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../shared/agent-session-journal-types'
|
||||
import { openTestAgentSessionRecordStore } from '../../runtime/agent-session-record-store-test-harness'
|
||||
import {
|
||||
closeTestJournalHostDatabase,
|
||||
openTestJournalHostDatabase
|
||||
} from '../agent-session-journal/journal-host-database-test-support'
|
||||
import { StructuredAgentSessionHost } from './structured-agent-session-host'
|
||||
import { abandonStructuredAgentSessionHost } from './structured-agent-session-host-test-abandon'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { agentJournalSubmissionKey } from '../../../shared/agent-session-journal-item-key'
|
||||
import {
|
||||
CALLER,
|
||||
attach,
|
||||
envelope,
|
||||
hostTestState
|
||||
hostTestState,
|
||||
hostTestRecoveryCapsuleSettled,
|
||||
replaceHostTestState
|
||||
} from './structured-agent-session-host-test-harness'
|
||||
import {
|
||||
HOST_TEST_SESSION as SESSION,
|
||||
@@ -57,44 +67,106 @@ it.each(['send', 'compact'] as const)(
|
||||
}
|
||||
)
|
||||
|
||||
it('rechecks a goal retry settled during startup, and replays it later without a child', async () => {
|
||||
const { host, store, acquire } = hostTestState()
|
||||
it('persists unknown Set objective and receipt across reopen, without replay overriding later actions', async () => {
|
||||
const { root, acquire } = hostTestState()
|
||||
let { host, store } = hostTestState()
|
||||
const changeThreadGoal = vi.fn(async () => ({ ok: true as const }))
|
||||
host.deps.adapter.changeThreadGoal = changeThreadGoal
|
||||
await attach()
|
||||
const change = { kind: 'set' as const, objective: 'Ship the parser' }
|
||||
let params = { envelope: envelope('agentSession.threadGoal', { change }), change }
|
||||
changeThreadGoal.mockRejectedValueOnce(new Error('not applied'))
|
||||
await expect(host.changeThreadGoal(CALLER, params)).rejects.toThrow('not applied')
|
||||
expect(await host.changeThreadGoal(CALLER, params)).toMatchObject({ ok: true, replayed: false })
|
||||
params = { envelope: envelope('agentSession.threadGoal', { change }), change }
|
||||
changeThreadGoal.mockRejectedValueOnce(new Error('not applied'))
|
||||
await expect(host.changeThreadGoal(CALLER, params)).rejects.toThrow('not applied')
|
||||
const params = { envelope: envelope('agentSession.threadGoal', { change }), change }
|
||||
changeThreadGoal.mockRejectedValueOnce(new Error('reply lost'))
|
||||
await expect(host.changeThreadGoal(CALLER, params)).rejects.toThrow('reply lost')
|
||||
expect(
|
||||
store.getOperationRow(CALLER.callerKey, params.envelope.clientOperationId)?.outcome
|
||||
).toEqual({ status: 'unknown' })
|
||||
const attempted = {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
sentAs: 'goal',
|
||||
blocks: [{ type: 'text', text: change.objective }]
|
||||
}
|
||||
const originalJournal = host.collaboratorsForTests().sessions.get(SESSION)!.journal
|
||||
await host.close(SESSION, 'evict')
|
||||
let ready!: () => void
|
||||
const open = vi.fn(
|
||||
() =>
|
||||
new Promise<{ reportedOptions: { model: string } }>((resolve) => {
|
||||
ready = () => resolve({ reportedOptions: { model: 'default' } })
|
||||
})
|
||||
)
|
||||
host.deps.adapter.open = open
|
||||
const retry = host.changeThreadGoal(CALLER, params)
|
||||
await vi.waitFor(() => expect(open).toHaveBeenCalledOnce())
|
||||
expect(changeThreadGoal).toHaveBeenCalledTimes(3)
|
||||
await store.recordOperationOutcome({
|
||||
callerKey: CALLER.callerKey,
|
||||
operationId: params.envelope.clientOperationId,
|
||||
outcome: { status: 'succeeded', sessionId: SESSION }
|
||||
await abandonStructuredAgentSessionHost(host)
|
||||
await hostTestRecoveryCapsuleSettled()
|
||||
const database = host.deps.journalDatabase
|
||||
closeTestJournalHostDatabase(root)
|
||||
expect(database.isClosed).toBe(true)
|
||||
store = await openTestAgentSessionRecordStore(root)
|
||||
host = new StructuredAgentSessionHost({
|
||||
...host.deps,
|
||||
store,
|
||||
journalDatabase: openTestJournalHostDatabase(root)
|
||||
})
|
||||
ready()
|
||||
expect(await retry).toMatchObject({ ok: true, replayed: true })
|
||||
expect(changeThreadGoal).toHaveBeenCalledTimes(3)
|
||||
expect(acquire).toHaveBeenCalledTimes(2)
|
||||
replaceHostTestState({ host, store })
|
||||
expect((await host.journalSnapshot(SESSION)).items.map((item) => item.body)).toContainEqual(
|
||||
attempted
|
||||
)
|
||||
expect(host.collaboratorsForTests().sessions.get(SESSION)!.journal).not.toBe(originalJournal)
|
||||
expect(
|
||||
store.getOperationRow(CALLER.callerKey, params.envelope.clientOperationId)?.outcome
|
||||
).toEqual({ status: 'unknown' })
|
||||
expect(await host.changeThreadGoal(CALLER, params)).toMatchObject({
|
||||
ok: false,
|
||||
refusal: { code: 'agent_session_operation_unknown' }
|
||||
})
|
||||
expect(changeThreadGoal).toHaveBeenCalledOnce()
|
||||
const latestGoal = {
|
||||
objective: 'Ship the newer goal',
|
||||
status: 'active',
|
||||
tokenBudget: null,
|
||||
tokensUsed: 0,
|
||||
timeUsedSeconds: 0,
|
||||
createdAt: 1,
|
||||
updatedAt: 2
|
||||
} as const
|
||||
const laterActions = [
|
||||
{ kind: 'status', status: 'paused' },
|
||||
{ kind: 'clear' },
|
||||
{ kind: 'set', objective: latestGoal.objective }
|
||||
] as const
|
||||
const acceptedActions = laterActions.map((later) => ({
|
||||
envelope: envelope('agentSession.threadGoal', { change: later }),
|
||||
change: later
|
||||
}))
|
||||
for (const accepted of acceptedActions) {
|
||||
expect(await host.changeThreadGoal(CALLER, accepted)).toMatchObject({ ok: true })
|
||||
if (accepted.change.kind === 'set') {
|
||||
acquire.mock.calls
|
||||
.at(-1)![0]
|
||||
.events!.appendItem(
|
||||
{ provider: 'orca', clientMessageId: 'reported-newer-goal' },
|
||||
{ kind: 'status', text: 'Goal', threadGoal: { state: 'set', goal: latestGoal } },
|
||||
{ turnScope: AGENT_JOURNAL_THREAD_SCOPE }
|
||||
)
|
||||
await host.flushStreamedEvents(SESSION)
|
||||
}
|
||||
const beforeReplay = await host.journalSnapshot(SESSION)
|
||||
expect(await host.changeThreadGoal(CALLER, params)).toMatchObject({
|
||||
ok: false,
|
||||
refusal: { code: 'agent_session_operation_unknown' }
|
||||
})
|
||||
expect(await host.journalSnapshot(SESSION)).toEqual(beforeReplay)
|
||||
}
|
||||
expect(host.collaboratorsForTests().sessions.get(SESSION)!.journal.threadGoal()).toEqual(
|
||||
latestGoal
|
||||
)
|
||||
expect(changeThreadGoal).toHaveBeenCalledTimes(4)
|
||||
await host.close(SESSION, 'evict')
|
||||
expect(await host.changeThreadGoal(CALLER, params)).toMatchObject({ ok: true, replayed: true })
|
||||
expect(await host.changeThreadGoal(CALLER, acceptedActions[1]!)).toMatchObject({
|
||||
ok: true,
|
||||
replayed: true
|
||||
})
|
||||
expect(await host.changeThreadGoal(CALLER, params)).toMatchObject({
|
||||
ok: false,
|
||||
refusal: { code: 'agent_session_operation_unknown' }
|
||||
})
|
||||
expect(acquire).toHaveBeenCalledTimes(2)
|
||||
expect(changeThreadGoal).toHaveBeenCalledTimes(3)
|
||||
expect(changeThreadGoal).toHaveBeenCalledTimes(4)
|
||||
expect(host.collaboratorsForTests().sessions.get(SESSION)!.journal.threadGoal()).toEqual(
|
||||
latestGoal
|
||||
)
|
||||
})
|
||||
|
||||
it.each([false, true])('keeps a goal behind active dispatch (ready child: %s)', async (ready) => {
|
||||
|
||||
+11
-79
@@ -10,11 +10,7 @@ import type {
|
||||
import type { AgentSessionJournal } from '../agent-session-journal/journal-store'
|
||||
import { createTrackedJournalOpener } from '../agent-session-journal/journal-host-database-test-support'
|
||||
import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter'
|
||||
import {
|
||||
journalRecordsThreadGoalChange,
|
||||
performThreadGoalChange,
|
||||
threadGoalPlan
|
||||
} from './structured-agent-session-thread-goal'
|
||||
import { performThreadGoalChange, threadGoalPlan } from './structured-agent-session-thread-goal'
|
||||
import type { AgentSessionTurnContext } from './structured-agent-session-turns'
|
||||
import { claudeAndCodexAgents } from './structured-agent-session-adapter-router-test-support'
|
||||
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
|
||||
@@ -141,7 +137,7 @@ describe('performThreadGoalChange', () => {
|
||||
expect(journal.snapshot().items).toEqual([])
|
||||
})
|
||||
|
||||
it('removes the objective when the provider request fails outright', async () => {
|
||||
it('retains the objective when a lost reply leaves application unknown', async () => {
|
||||
const journal = await openJournal()
|
||||
const ctx = context(journal, {
|
||||
changeThreadGoal: async () => {
|
||||
@@ -155,39 +151,8 @@ describe('performThreadGoalChange', () => {
|
||||
change: { kind: 'set', objective: 'Ship the parser' }
|
||||
})
|
||||
).rejects.toThrow('connection closed')
|
||||
expect(journal.snapshot().items).toEqual([])
|
||||
})
|
||||
|
||||
it('puts the objective back, once, when a withdrawn set runs again', async () => {
|
||||
const journal = await openJournal()
|
||||
const attempts: (() => Promise<{ ok: true }>)[] = [
|
||||
async () => {
|
||||
throw new Error('request timed out')
|
||||
},
|
||||
async () => ({ ok: true as const })
|
||||
]
|
||||
const ctx = context(journal, {
|
||||
changeThreadGoal: () => attempts.shift()!()
|
||||
})
|
||||
const input = {
|
||||
clientOperationId: 'op-9',
|
||||
change: { kind: 'set' as const, objective: 'Ship the parser' }
|
||||
}
|
||||
|
||||
await expect(performThreadGoalChange(ctx, input)).rejects.toThrow('request timed out')
|
||||
expect(journal.snapshot().items).toEqual([])
|
||||
|
||||
await expect(performThreadGoalChange(ctx, input)).resolves.toEqual({
|
||||
ok: true,
|
||||
value: { change: 'set' }
|
||||
})
|
||||
expect(journal.snapshot().items.map((item) => item.body)).toEqual([
|
||||
{
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: 'Ship the parser' }],
|
||||
sentAs: 'goal'
|
||||
}
|
||||
expect.objectContaining({ sentAs: 'goal' })
|
||||
])
|
||||
})
|
||||
|
||||
@@ -292,51 +257,18 @@ describe('threadGoalPlan replay', () => {
|
||||
payloadFingerprint: 'fp'
|
||||
}
|
||||
|
||||
it('answers a lost response from the goal the journal records, and runs again otherwise', async () => {
|
||||
it('replays only recorded success, never rerunning an unknown operation over a later decision', async () => {
|
||||
const journal = await openJournal()
|
||||
const ctx = context(journal, {})
|
||||
const paused = threadGoalPlan({ envelope, change: { kind: 'status', status: 'paused' } })
|
||||
const set = threadGoalPlan({ envelope, change: { kind: 'set', objective: 'Ship the parser' } })
|
||||
const clear = threadGoalPlan({ envelope, change: { kind: 'clear' } })
|
||||
const unknown = { status: 'unknown' as const }
|
||||
|
||||
expect(paused.recoverUnknownFromDurableState).toBe(true)
|
||||
expect(paused.rerunWhenReplayMissing?.(ctx)).toBe(true)
|
||||
expect(paused.replay(ctx, unknown)).toBeNull()
|
||||
expect(set.replay(ctx, unknown)).toBeNull()
|
||||
expect(clear.replay(ctx, unknown)).toEqual({ change: 'clear' })
|
||||
|
||||
const plan = threadGoalPlan({ envelope, change: { kind: 'set', objective: GOAL.objective } })
|
||||
await appendGoalRow(journal, {})
|
||||
expect(plan.replay(ctx, { status: 'unknown' })).toBeNull()
|
||||
await appendGoalRow(journal, { status: 'paused' })
|
||||
expect(paused.replay(ctx, unknown)).toEqual({ change: 'status' })
|
||||
expect(set.replay(ctx, unknown)).toBeNull()
|
||||
expect(clear.replay(ctx, unknown)).toBeNull()
|
||||
|
||||
expect(set.replay(ctx, { status: 'succeeded', sessionId: 'session-1' })).toEqual({
|
||||
expect(plan.replay(ctx, { status: 'unknown' })).toBeNull()
|
||||
expect(plan.rerunWhenReplayMissing?.(ctx)).not.toBe(true)
|
||||
expect(plan.recoverUnknownFromDurableState).not.toBe(true)
|
||||
expect(plan.replay(ctx, { status: 'succeeded', sessionId: 'session-1' })).toEqual({
|
||||
change: 'set'
|
||||
})
|
||||
expect(
|
||||
set.replay(ctx, { status: 'failed', code: 'agent_session_operation_invalid' })
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('reads a set as applied only when the recorded goal is that objective, active', () => {
|
||||
const change = { kind: 'set' as const, objective: 'Ship the parser' }
|
||||
expect(journalRecordsThreadGoalChange(GOAL, change)).toBe(true)
|
||||
expect(journalRecordsThreadGoalChange({ ...GOAL, status: 'paused' }, change)).toBe(false)
|
||||
expect(journalRecordsThreadGoalChange({ ...GOAL, objective: 'Ship it' }, change)).toBe(false)
|
||||
expect(journalRecordsThreadGoalChange(null, change)).toBe(false)
|
||||
})
|
||||
|
||||
it('reads a status change as applied only when the recorded goal is in that status', () => {
|
||||
const pause = { kind: 'status' as const, status: 'paused' as const }
|
||||
expect(journalRecordsThreadGoalChange({ ...GOAL, status: 'paused' }, pause)).toBe(true)
|
||||
expect(journalRecordsThreadGoalChange(GOAL, pause)).toBe(false)
|
||||
expect(
|
||||
journalRecordsThreadGoalChange(
|
||||
{ ...GOAL, status: 'paused' },
|
||||
{ kind: 'status', status: 'active' }
|
||||
)
|
||||
).toBe(false)
|
||||
expect(journalRecordsThreadGoalChange(null, pause)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,21 +48,6 @@ function objectiveIdentity(clientOperationId: string): AgentJournalItemIdentity
|
||||
return { provider: 'orca', clientMessageId: `thread-goal:${clientOperationId}` }
|
||||
}
|
||||
|
||||
/** Provider-journaled transitions recover operations whose response was lost. */
|
||||
export function journalRecordsThreadGoalChange(
|
||||
goal: AgentJournalThreadGoal | null,
|
||||
change: AgentSessionThreadGoalChange
|
||||
): boolean {
|
||||
switch (change.kind) {
|
||||
case 'clear':
|
||||
return goal === null
|
||||
case 'status':
|
||||
return goal !== null && goal.status === change.status
|
||||
case 'set':
|
||||
return goal !== null && goal.status === 'active' && goal.objective === change.objective
|
||||
}
|
||||
}
|
||||
|
||||
export async function performThreadGoalChange(
|
||||
ctx: AgentSessionTurnContext,
|
||||
input: { clientOperationId: string; change: AgentSessionThreadGoalChange; signal?: AbortSignal }
|
||||
@@ -99,20 +84,20 @@ export async function performThreadGoalChange(
|
||||
await ctx.journal.appendTombstone(identity, { fence: ctx.fence })
|
||||
}
|
||||
}
|
||||
let result: Awaited<ReturnType<typeof ctx.adapter.changeThreadGoal>>
|
||||
try {
|
||||
throwIfSignalAborted(input.signal)
|
||||
result = await ctx.adapter.changeThreadGoal({
|
||||
sessionId: ctx.sessionId,
|
||||
fence: ctx.fence,
|
||||
change,
|
||||
replacesGoal,
|
||||
signal: input.signal
|
||||
})
|
||||
} catch (error) {
|
||||
await withdrawObjective()
|
||||
throw error
|
||||
}
|
||||
// A lost response cannot prove non-application; retain the objective for provider reconciliation.
|
||||
const result = await ctx.adapter.changeThreadGoal({
|
||||
sessionId: ctx.sessionId,
|
||||
fence: ctx.fence,
|
||||
change,
|
||||
replacesGoal,
|
||||
signal: input.signal
|
||||
})
|
||||
if (!result.ok) {
|
||||
await withdrawObjective()
|
||||
return refused('providerRejected', result.rejected)
|
||||
@@ -135,15 +120,8 @@ export function threadGoalPlan(params: {
|
||||
change: params.change,
|
||||
signal: params.signal
|
||||
}),
|
||||
// Recover lost responses from provider-journaled state; otherwise every change is safe to repeat.
|
||||
recoverUnknownFromDurableState: true,
|
||||
replay: (ctx, outcome) =>
|
||||
outcome.status === 'succeeded' ||
|
||||
(outcome.status === 'unknown' &&
|
||||
journalRecordsThreadGoalChange(ctx.journal.threadGoal(), params.change))
|
||||
? value
|
||||
: null,
|
||||
rerunWhenReplayMissing: () => true
|
||||
// Current state cannot identify an old operation after a later user action changed the goal.
|
||||
replay: (_ctx, outcome) => (outcome.status === 'succeeded' ? value : null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ function goal(overrides: Partial<AgentJournalThreadGoal> = {}): AgentJournalThre
|
||||
function renderBanner(
|
||||
value: AgentJournalThreadGoal,
|
||||
pending = false,
|
||||
runningTurn: { startedAt: number | null } | null = { startedAt: null }
|
||||
runningTurn: { startedAt: number | null } | null = { startedAt: null },
|
||||
startsHeld = false
|
||||
) {
|
||||
const onChange = vi.fn()
|
||||
const view = render(
|
||||
@@ -39,6 +40,7 @@ function renderBanner(
|
||||
goal={value}
|
||||
pending={pending}
|
||||
hostChanges
|
||||
startsHeld={startsHeld}
|
||||
sendUnavailable={false}
|
||||
isVisible
|
||||
runningTurn={runningTurn}
|
||||
@@ -100,15 +102,27 @@ describe('NativeChatThreadGoalBanner', () => {
|
||||
expect(screen.queryByRole('button', { name: 'Resume goal' })).toBeNull()
|
||||
})
|
||||
|
||||
it.each(['paused', 'blocked'] as const)(
|
||||
'admits pending %s Resume but honors a permission hold',
|
||||
(status) => {
|
||||
const { onChange, view } = renderBanner(goal({ status }), true)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Resume goal' }))
|
||||
expect(onChange).toHaveBeenCalledWith({ kind: 'status', status: 'active' })
|
||||
view.unmount()
|
||||
renderBanner(goal({ status }), true, null, true)
|
||||
expect(screen.getByRole('button', { name: 'Resume goal' })).toBeDisabled()
|
||||
}
|
||||
)
|
||||
|
||||
it('renders nothing for a completed goal', () => {
|
||||
const { view } = renderBanner(goal({ status: 'complete' }))
|
||||
expect(view.container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('disables the goal commands while one is in flight, but not the expand toggle', () => {
|
||||
it('keeps Pause and Clear available while another goal change is in flight', () => {
|
||||
renderBanner(goal(), true)
|
||||
expect(screen.getByRole('button', { name: 'Clear goal' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'Pause goal' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'Clear goal' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'Pause goal' })).toBeEnabled()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show full goal' }))
|
||||
expect(screen.getByRole('button', { name: 'Hide full goal' })).toBeEnabled()
|
||||
|
||||
@@ -57,7 +57,7 @@ export function NativeChatThreadGoalBanner(props: {
|
||||
runningTurn: { startedAt: number | null } | null
|
||||
onChange: (change: AgentSessionThreadGoalChange) => void
|
||||
}): React.JSX.Element | null {
|
||||
const { goal, pending, onChange } = props
|
||||
const { goal, onChange } = props
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const { runningTurn } = props
|
||||
const now = useNow(1_000, props.isVisible && goal.status === 'active' && runningTurn !== null)
|
||||
@@ -91,10 +91,7 @@ export function NativeChatThreadGoalBanner(props: {
|
||||
<GoalAction
|
||||
label={translate('components.native-chat.goal.clear', 'Clear goal')}
|
||||
// Claude's `/goal clear` must enter a fresh turn, like a composer send.
|
||||
disabled={
|
||||
pending ||
|
||||
(!props.hostChanges && (props.sendUnavailable || props.startsHeld === true))
|
||||
}
|
||||
disabled={!props.hostChanges && (props.sendUnavailable || props.startsHeld === true)}
|
||||
onClick={() => onChange({ kind: 'clear' })}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
@@ -102,7 +99,7 @@ export function NativeChatThreadGoalBanner(props: {
|
||||
{statusChange === 'paused' ? (
|
||||
<GoalAction
|
||||
label={translate('components.native-chat.goal.pause', 'Pause goal')}
|
||||
disabled={pending}
|
||||
disabled={false}
|
||||
onClick={() => onChange({ kind: 'status', status: 'paused' })}
|
||||
>
|
||||
<Pause className="size-3.5" />
|
||||
@@ -110,7 +107,7 @@ export function NativeChatThreadGoalBanner(props: {
|
||||
) : statusChange === 'active' ? (
|
||||
<GoalAction
|
||||
label={translate('components.native-chat.goal.resume', 'Resume goal')}
|
||||
disabled={pending || props.startsHeld === true}
|
||||
disabled={props.startsHeld === true}
|
||||
onClick={() => onChange({ kind: 'status', status: 'active' })}
|
||||
>
|
||||
<Play className="size-3.5" />
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
type AgentSessionWriteNoticePart,
|
||||
type AgentSessionWriteNoticeSentence
|
||||
} from '../../../../shared/agent-session-write-notice-copy'
|
||||
import { agentSessionWriteNoticeText } from './agent-session-write-notice-text'
|
||||
import {
|
||||
agentSessionWriteFailureText,
|
||||
agentSessionWriteNoticeText
|
||||
} from './agent-session-write-notice-text'
|
||||
|
||||
const SENTENCES = Object.keys(AGENT_SESSION_WRITE_NOTICE_COPY).filter(
|
||||
(key): key is AgentSessionWriteNoticeSentence => key in AGENT_SESSION_WRITE_NOTICE_COPY
|
||||
@@ -36,6 +39,22 @@ describe('desktop words for a write that did not happen', () => {
|
||||
expect(en.components['native-chat'].writeNotice).toEqual(AGENT_SESSION_WRITE_NOTICE_COPY)
|
||||
})
|
||||
|
||||
it('does not promise the old goal survived a refused replacement, including older translations', async () => {
|
||||
for (const language of ['en', 'fr']) {
|
||||
await i18n.changeLanguage(language)
|
||||
expect(
|
||||
agentSessionWriteFailureText(
|
||||
{
|
||||
kind: 'refused',
|
||||
code: 'agent_session_operation_invalid',
|
||||
details: { reason: 'providerRejected' }
|
||||
},
|
||||
'goal'
|
||||
)
|
||||
).toContain("Couldn't finish changing the goal. Check the chat.")
|
||||
}
|
||||
})
|
||||
|
||||
it('translates each sentence whole and shows a provider reason as written', async () => {
|
||||
await i18n.changeLanguage('fr')
|
||||
expect(agentSessionWriteNoticeText(['restartFailed', 'notDoneSend'])).toBe(
|
||||
|
||||
@@ -37,7 +37,11 @@ const SENTENCES: Record<AgentSessionWriteNoticeSentence, () => string> = {
|
||||
translate('components.native-chat.writeNotice.notDoneOption', COPY.notDoneOption),
|
||||
notDoneCommand: () =>
|
||||
translate('components.native-chat.writeNotice.notDoneCommand', COPY.notDoneCommand),
|
||||
notDoneGoal: () => translate('components.native-chat.writeNotice.notDoneGoal', COPY.notDoneGoal),
|
||||
notDoneGoalChangeIncomplete: () =>
|
||||
translate(
|
||||
'components.native-chat.writeNotice.notDoneGoalChangeIncomplete',
|
||||
COPY.notDoneGoalChangeIncomplete
|
||||
),
|
||||
notDoneQueuedEdit: () =>
|
||||
translate('components.native-chat.writeNotice.notDoneQueuedEdit', COPY.notDoneQueuedEdit),
|
||||
restartFailed: () =>
|
||||
|
||||
+22
-18
@@ -104,28 +104,32 @@ describe('useStructuredAgentSessionThreadGoal', () => {
|
||||
expect(cleared.result.current?.goal).toBeNull()
|
||||
})
|
||||
|
||||
it('serializes changes: a second submit while one is unsettled is answered false, not sent', async () => {
|
||||
let settle: (value: { change: 'set' } | null) => void = () => undefined
|
||||
const { mutate, calls } = mutateWith(
|
||||
() => new Promise<{ change: 'set' } | null>((resolve) => (settle = resolve))
|
||||
)
|
||||
const { result } = harness({ support: { current: null }, mutate })
|
||||
|
||||
let first: Promise<boolean> = Promise.resolve(false)
|
||||
let second: Promise<boolean> = Promise.resolve(false)
|
||||
it('admits Pause and Clear while Set is unsettled, keeping pending until all settle', async () => {
|
||||
const set = Promise.withResolvers<unknown>()
|
||||
const pause = Promise.withResolvers<unknown>()
|
||||
const clear = Promise.withResolvers<unknown>()
|
||||
const answers = [set, pause, clear]
|
||||
const { mutate, calls } = mutateWith(() => answers.shift()!.promise)
|
||||
const { result } = harness({ support: { current: GOAL }, mutate })
|
||||
let changes: Promise<boolean>[] = []
|
||||
act(() => {
|
||||
first = result.current!.change({ kind: 'set', objective: 'Ship the parser' })
|
||||
second = result.current!.change({ kind: 'set', objective: 'Ship the parser' })
|
||||
changes = [
|
||||
result.current!.change({ kind: 'set', objective: 'New objective' }),
|
||||
result.current!.change({ kind: 'status', status: 'paused' }),
|
||||
result.current!.change({ kind: 'clear' })
|
||||
]
|
||||
})
|
||||
await expect(second).resolves.toBe(false)
|
||||
expect(calls).toHaveBeenCalledTimes(1)
|
||||
expect(calls).toHaveBeenCalledWith('agentSession.threadGoal', 'agentSession.threadGoal', {
|
||||
change: { kind: 'set', objective: 'Ship the parser' }
|
||||
expect(calls).toHaveBeenCalledTimes(3)
|
||||
expect(result.current?.pending).toBe(true)
|
||||
await act(async () => {
|
||||
clear.resolve({ change: 'clear' })
|
||||
pause.resolve({ change: 'status' })
|
||||
})
|
||||
expect(result.current?.pending).toBe(true)
|
||||
|
||||
await act(async () => settle({ change: 'set' }))
|
||||
await expect(first).resolves.toBe(true)
|
||||
await act(async () => {
|
||||
set.resolve({ change: 'set' })
|
||||
await expect(Promise.all(changes)).resolves.toEqual([true, true, true])
|
||||
})
|
||||
expect(result.current?.pending).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
+5
-11
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import type {
|
||||
AgentJournalRenderItem,
|
||||
AgentJournalThreadGoal
|
||||
@@ -33,8 +33,7 @@ export function useStructuredAgentSessionThreadGoal(args: {
|
||||
send: (text: string) => boolean | 'queued'
|
||||
}): StructuredAgentSessionThreadGoal | null {
|
||||
const { journalItems, mutate, support, reported, journalEpoch, send } = args
|
||||
const [pending, setPending] = useState(false)
|
||||
const pendingRef = useRef(false)
|
||||
const [pendingCount, setPendingCount] = useState(0)
|
||||
const report = support ?? reported
|
||||
// Loaded rows reach the live head and outrank older whole-journal goal evidence.
|
||||
const floor =
|
||||
@@ -52,11 +51,7 @@ export function useStructuredAgentSessionThreadGoal(args: {
|
||||
if (!hostChanges) {
|
||||
return next.kind === 'clear' && send(CLEAR_GOAL_COMMAND) !== false
|
||||
}
|
||||
if (pendingRef.current) {
|
||||
return false
|
||||
}
|
||||
pendingRef.current = true
|
||||
setPending(true)
|
||||
setPendingCount((count) => count + 1)
|
||||
try {
|
||||
const result = await mutate<AgentSessionThreadGoalResult>(
|
||||
'agentSession.threadGoal',
|
||||
@@ -65,8 +60,7 @@ export function useStructuredAgentSessionThreadGoal(args: {
|
||||
)
|
||||
return result !== null
|
||||
} finally {
|
||||
pendingRef.current = false
|
||||
setPending(false)
|
||||
setPendingCount((count) => count - 1)
|
||||
}
|
||||
},
|
||||
[hostChanges, mutate, send]
|
||||
@@ -74,5 +68,5 @@ export function useStructuredAgentSessionThreadGoal(args: {
|
||||
if (report === undefined || (!hostChanges && goal === null)) {
|
||||
return null
|
||||
}
|
||||
return { goal, pending, hostChanges, change }
|
||||
return { goal, pending: pendingCount > 0, hostChanges, change }
|
||||
}
|
||||
|
||||
+1
-1
@@ -3314,7 +3314,7 @@
|
||||
"notAvailable": "This isn't available in this chat.",
|
||||
"notDoneAnswer": "Your answer was not sent.",
|
||||
"notDoneCommand": "The command didn't run.",
|
||||
"notDoneGoal": "The goal wasn't changed.",
|
||||
"notDoneGoalChangeIncomplete": "Couldn't finish changing the goal. Check the chat.",
|
||||
"notDoneOption": "The setting wasn't changed.",
|
||||
"notDoneQueuedEdit": "Your edit wasn't saved.",
|
||||
"notDoneReadHistory": "This chat's history couldn't be loaded.",
|
||||
|
||||
@@ -19059,7 +19059,7 @@
|
||||
"notDoneAnswer": "Your answer was not sent.",
|
||||
"notDoneOption": "The setting wasn't changed.",
|
||||
"notDoneCommand": "The command didn't run.",
|
||||
"notDoneGoal": "The goal wasn't changed.",
|
||||
"notDoneGoalChangeIncomplete": "Couldn't finish changing the goal. Check the chat.",
|
||||
"notDoneQueuedEdit": "Your edit wasn't saved.",
|
||||
"restartFailed": "The agent couldn't restart.",
|
||||
"capacity": "Orca on the computer running this chat has hit a request limit. Update Orca there, then try again.",
|
||||
|
||||
@@ -18728,7 +18728,6 @@
|
||||
"notDoneAnswer": "Su respuesta no se envió.",
|
||||
"notDoneOption": "La configuración no se cambió.",
|
||||
"notDoneCommand": "El comando no se ejecutó.",
|
||||
"notDoneGoal": "El objetivo no se cambió.",
|
||||
"notDoneQueuedEdit": "Tu edición no se guardó.",
|
||||
"restartFailed": "El agente no pudo reiniciarse.",
|
||||
"capacity": "Orca en el equipo que ejecuta este chat alcanzó un límite de solicitudes. Actualice Orca en ese equipo y vuelva a intentarlo.",
|
||||
|
||||
@@ -18868,7 +18868,6 @@
|
||||
"notDoneAnswer": "Votre réponse n'a pas été envoyée.",
|
||||
"notDoneOption": "Le paramètre n'a pas été modifié.",
|
||||
"notDoneCommand": "La commande n'a pas été exécutée.",
|
||||
"notDoneGoal": "L'objectif n'a pas été modifié.",
|
||||
"notDoneQueuedEdit": "Votre modification n'a pas été enregistrée.",
|
||||
"restartFailed": "L'agent n'a pas pu redémarrer.",
|
||||
"capacity": "Orca sur l'ordinateur qui exécute ce chat a atteint une limite de requêtes. Mettez à jour Orca sur cet ordinateur, puis réessayez.",
|
||||
|
||||
@@ -18758,7 +18758,6 @@
|
||||
"notDoneAnswer": "回答は送信されませんでした。",
|
||||
"notDoneOption": "設定は変更されませんでした。",
|
||||
"notDoneCommand": "コマンドは実行されませんでした。",
|
||||
"notDoneGoal": "目標は変更されませんでした。",
|
||||
"notDoneQueuedEdit": "編集内容は保存されていません。",
|
||||
"restartFailed": "Agent を再起動できませんでした。",
|
||||
"capacity": "このチャットを実行しているコンピューターの Orca がリクエストの上限に達しました。そのコンピューターで Orca を更新してから、もう一度お試しください。",
|
||||
|
||||
@@ -18758,7 +18758,6 @@
|
||||
"notDoneAnswer": "답변이 전송되지 않았습니다.",
|
||||
"notDoneOption": "설정이 변경되지 않았습니다.",
|
||||
"notDoneCommand": "명령이 실행되지 않았습니다.",
|
||||
"notDoneGoal": "목표가 변경되지 않았습니다.",
|
||||
"notDoneQueuedEdit": "편집한 내용은 저장되지 않았습니다.",
|
||||
"restartFailed": "에이전트를 다시 시작하지 못했습니다.",
|
||||
"capacity": "이 채팅을 실행하는 컴퓨터의 Orca가 요청 한도에 도달했습니다. 해당 컴퓨터에서 Orca를 업데이트한 다음 다시 시도하세요.",
|
||||
|
||||
@@ -19703,7 +19703,6 @@
|
||||
"notDoneAnswer": "Sua resposta não foi enviada.",
|
||||
"notDoneOption": "A configuração não foi alterada.",
|
||||
"notDoneCommand": "O comando não foi executado.",
|
||||
"notDoneGoal": "O objetivo não foi alterado.",
|
||||
"notDoneQueuedEdit": "Sua edição não foi salva.",
|
||||
"restartFailed": "Não foi possível reiniciar o agente.",
|
||||
"capacity": "O Orca no computador que executa este chat atingiu um limite de requisições. Atualize o Orca lá e tente novamente.",
|
||||
|
||||
@@ -18723,7 +18723,6 @@
|
||||
"notDoneAnswer": "您的回答未发送。",
|
||||
"notDoneOption": "设置未更改。",
|
||||
"notDoneCommand": "命令未运行。",
|
||||
"notDoneGoal": "目标未更改。",
|
||||
"notDoneQueuedEdit": "你的编辑未保存。",
|
||||
"restartFailed": "智能体无法重新启动。",
|
||||
"capacity": "运行此聊天的计算机上的 Orca 已达到请求上限。请在那台计算机上更新 Orca,然后重试。",
|
||||
|
||||
@@ -94,7 +94,7 @@ const NOT_DONE: Record<AgentSessionWriteKind, AgentSessionWriteNoticeSentence> =
|
||||
command: 'notDoneCommand',
|
||||
clear: 'notDoneCommand',
|
||||
compact: 'notDoneCommand',
|
||||
goal: 'notDoneGoal',
|
||||
goal: 'notDoneGoalChangeIncomplete',
|
||||
'queued-edit': 'notDoneQueuedEdit'
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ const NOT_DONE: Record<AgentSessionWriteKind, AgentSessionWriteNoticeSentence> =
|
||||
'queued-edit': 'notDoneQueuedEdit',
|
||||
clear: 'notDoneCommand',
|
||||
compact: 'notDoneCommand',
|
||||
goal: 'notDoneGoal'
|
||||
goal: 'notDoneGoalChangeIncomplete'
|
||||
}
|
||||
|
||||
/** Share actionable wording for /clear and /compact blocked by running work. */
|
||||
|
||||
@@ -28,7 +28,7 @@ export const AGENT_SESSION_WRITE_NOTICE_COPY = {
|
||||
notDoneAnswer: 'Your answer was not sent.',
|
||||
notDoneOption: "The setting wasn't changed.",
|
||||
notDoneCommand: "The command didn't run.",
|
||||
notDoneGoal: "The goal wasn't changed.",
|
||||
notDoneGoalChangeIncomplete: "Couldn't finish changing the goal. Check the chat.",
|
||||
notDoneQueuedEdit: "Your edit wasn't saved.",
|
||||
restartFailed: "The agent couldn't restart.",
|
||||
capacity:
|
||||
|
||||
Reference in New Issue
Block a user