@openchambery/web 1.19.15-beta.4 → 1.19.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AssistantView-bxT9mr97.js → AssistantView-xdL1EvpR.js} +2 -2
- package/dist/assets/{DiagramView-DJPrdklU.js → DiagramView-DPgkYU-_.js} +1 -1
- package/dist/assets/MarkdownRenderer-CP7UVZdr.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-CBdwjEMH.js → MarkdownRendererImpl-BB1Q1RWk.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-CHwYXkzv.js → MarkstreamRendererImpl-BjQrUIcq.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DLmATOi0.js → MobileDetailNavigation-Cyzs6cuR.js} +1 -1
- package/dist/assets/{MobileShareBridge-BbvoAeXg.js → MobileShareBridge-A-Z6jMUn.js} +1 -1
- package/dist/assets/MobileSurface-DjDa20gx.js +2 -0
- package/dist/assets/{MultiRunWindow-B7XPMkdc.js → MultiRunWindow-Cg5x-kJV.js} +1 -1
- package/dist/assets/{SettingsView-DABJm3u8.js → SettingsView-CYrTHDdp.js} +1 -1
- package/dist/assets/{SettingsWindow-D7n0m-52.js → SettingsWindow-CF-0Zgxr.js} +1 -1
- package/dist/assets/{TerminalView-DERnAkUi.js → TerminalView-CShou18D.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Bx3plCCc.js → ToolOutputDialog-BvmxlNOa.js} +1 -1
- package/dist/assets/{appThemeRegistry-BdHiNL-J.js → appThemeRegistry-iqSTUpPj.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-B3sIMQCF.js → fileMentionTouchSelection-CddGH2aH.js} +1 -1
- package/dist/assets/{gitApi-CDc5ynsh.js → gitApi-BwuaHHRQ.js} +1 -1
- package/dist/assets/{insertionBoundaries-3yNePddt.js → insertionBoundaries-og6NBgdn.js} +1 -1
- package/dist/assets/{main-5GGJlyEB.js → main-31g3JiQx.js} +4 -4
- package/dist/assets/{main-C_21O0IU.js → main-VD-QBM47.js} +2 -2
- package/dist/assets/{miniChat-Ti_tFK96.js → miniChat-DwCMTamY.js} +2 -2
- package/dist/assets/{mobile-BUYP8DBL.js → mobile-C7q1yowe.js} +2 -2
- package/dist/assets/{multirun-CXazK6qX.js → multirun-DcN5T0lN.js} +1 -1
- package/dist/assets/{projectMeta-sXx-r5jn.js → projectMeta-moNJYCK4.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-CLx8jNP4.js → renderElectronMiniChatApp-DVkgKA6A.js} +2 -2
- package/dist/assets/{renderMobileApp-BGKxwMFU.js → renderMobileApp-BgEXZhCO.js} +4 -4
- package/dist/assets/{runtimeConfig-YCy1Bcb2.js → runtimeConfig-B49cxtgI.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-Dh3YvmRM.js → runtimeEndpointReset-9K_Dj1qR.js} +1 -1
- package/dist/assets/{sessionLookup-BzkA2pF0.js → sessionLookup-m3h8c4xR.js} +1 -1
- package/dist/assets/{useAppFontEffects-BLw4Av7h.js → useAppFontEffects-DRFi4TCE.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-DNDIYvwP.js → useDesktopWindowControlsLayout-BdNR3zMN.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-B_3sINMx.js → useEffectiveDirectory-COoPja_2.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-BBVnSn9p.js → useMobileNavigationStore-Cn0-ozUB.js} +1 -1
- package/dist/assets/{useSessionAutoCleanup-SsGxsSRB.js → useSessionAutoCleanup-XwgyzsEd.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-WCuVI6O_.js → useWorkerHighlightedLines-GgcqEoKY.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +31 -6
- package/server/lib/assistants/contact-tools.js +1 -16
- package/server/lib/assistants/contact-tools.test.js +0 -27
- package/server/lib/assistants/harness.js +140 -133
- package/server/lib/assistants/harness.test.js +207 -43
- package/server/lib/assistants/service.js +10 -33
- package/server/lib/assistants/service.test.js +75 -3
- package/server/lib/llm/DOCUMENTATION.md +8 -1
- package/server/lib/llm/completions.js +5 -0
- package/server/lib/llm/completions.test.js +19 -0
- package/server/lib/llm/generate.js +5 -1
- package/server/lib/llm/generate.test.js +23 -0
- package/dist/assets/MarkdownRenderer-4gjE7aM5.js +0 -2
- package/dist/assets/MobileSurface-BBOx43VU.js +0 -2
|
@@ -3,13 +3,10 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { describe, expect, it, vi } from 'vitest'
|
|
5
5
|
import {
|
|
6
|
-
CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
7
6
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
8
7
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
9
8
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
10
9
|
MISSED_FENCE_RETRY_USER_TEXT,
|
|
11
|
-
MISSED_TOOL_FAILURE_BUBBLE,
|
|
12
|
-
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
13
10
|
NEW_CONVERSATION_TOOL_NAME,
|
|
14
11
|
createContactTools,
|
|
15
12
|
} from './contact-tools.js'
|
|
@@ -22,6 +19,161 @@ import {
|
|
|
22
19
|
import { PI_CODING_TOOL_NAMES } from './pi-tools.js'
|
|
23
20
|
|
|
24
21
|
describe('createContactStreamFn', () => {
|
|
22
|
+
it('forwards the assistant variant while keeping private reasoning out of bubbles', async () => {
|
|
23
|
+
const onBubbleDelta = vi.fn();
|
|
24
|
+
const completion = vi.fn(async ({ onTextDelta }) => {
|
|
25
|
+
onTextDelta('Private reasoning');
|
|
26
|
+
return { text: '```openchamber-final\n{"status":"complete","text":"已完成。"}\n```' };
|
|
27
|
+
});
|
|
28
|
+
const result = await runContactTurn({
|
|
29
|
+
assistant: { providerID: 'p', modelID: 'm', variant: 'high' },
|
|
30
|
+
history: [], userText: '你好', createChatCompletion: completion, onBubbleDelta,
|
|
31
|
+
});
|
|
32
|
+
expect(completion.mock.calls.length).toBeGreaterThan(0);
|
|
33
|
+
for (const [request] of completion.mock.calls) expect(request.body.variant).toBe('high');
|
|
34
|
+
expect(result.bubbles).toEqual(['已完成。']);
|
|
35
|
+
expect(JSON.stringify(onBubbleDelta.mock.calls)).not.toContain('Private reasoning');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('publishes a model-authored message while generation is pending, before the tool or final response', async () => {
|
|
39
|
+
let release;
|
|
40
|
+
const gate = new Promise((resolve) => { release = resolve })
|
|
41
|
+
const onBubbleDelta = vi.fn()
|
|
42
|
+
const prefix = '```openchamber-message\n{"text":"我先检查配置,再保存这次修改。"}\n```\n'
|
|
43
|
+
const tool = '```openchamber-tool\n{"name":"read","arguments":{"path":"config.json"}}\n```'
|
|
44
|
+
const createChatCompletion = vi.fn(async ({ onTextDelta }) => {
|
|
45
|
+
for (const char of prefix) onTextDelta(char)
|
|
46
|
+
await gate
|
|
47
|
+
onTextDelta(tool)
|
|
48
|
+
return { text: prefix + tool }
|
|
49
|
+
})
|
|
50
|
+
const stream = createContactStreamFn(createChatCompletion, { onBubbleDelta })(
|
|
51
|
+
{ name: 'p/m', id: 'm', provider: 'openchamber', api: 'openai-completions' },
|
|
52
|
+
{ messages: [], tools: [{ name: 'read' }] },
|
|
53
|
+
)
|
|
54
|
+
try {
|
|
55
|
+
await vi.waitFor(() => expect(onBubbleDelta).toHaveBeenCalledWith(0, '我先检查配置,再保存这次修改。', true))
|
|
56
|
+
expect(onBubbleDelta).toHaveBeenCalledTimes(1)
|
|
57
|
+
} finally { release() }
|
|
58
|
+
const result = await stream.result()
|
|
59
|
+
expect(projectStreamedContactTurnBubbles([result])).toEqual(['我先检查配置,再保存这次修改。'])
|
|
60
|
+
expect(result.content.at(-1)).toMatchObject({ type: 'toolCall', name: 'read' })
|
|
61
|
+
expect(onBubbleDelta).toHaveBeenCalledTimes(1)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('keeps raw tokens, reasoning, partial public records and nested tool examples private', async () => {
|
|
65
|
+
const onBubbleDelta = vi.fn()
|
|
66
|
+
const controller = new AbortController()
|
|
67
|
+
const stream = createContactStreamFn(async ({ onTextDelta }) => {
|
|
68
|
+
onTextDelta('Private reasoning\n```openchamber-message\n{"text":"not public"}\n```')
|
|
69
|
+
controller.abort()
|
|
70
|
+
return { text: '' }
|
|
71
|
+
}, { onBubbleDelta, signal: controller.signal })(
|
|
72
|
+
{ name: 'p/m', id: 'm', provider: 'openchamber', api: 'openai-completions' }, { messages: [] },
|
|
73
|
+
)
|
|
74
|
+
await stream.result()
|
|
75
|
+
expect(onBubbleDelta).not.toHaveBeenCalled()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('uses the model reply after saving a prompt and keeps the stock tool confirmation private', async () => {
|
|
79
|
+
const tools = createContactTools({
|
|
80
|
+
updateAssistantSettings: async () => ({ updated: true, defaultPrompt: '简洁回复' }),
|
|
81
|
+
currentAssistant: { id: 'a', name: 'Config' },
|
|
82
|
+
})
|
|
83
|
+
const completion = vi.fn(async ({ body }) => {
|
|
84
|
+
const result = body.messages.find((m) => m.content.includes('OpenChamber tool result name=update_default_prompt'))
|
|
85
|
+
if (result) {
|
|
86
|
+
expect(result.content).toContain('Default prompt saved.')
|
|
87
|
+
return { text: '```openchamber-final\n{"status":"complete","text":"提示词已保存,接下来的对话会按这个设置回复。"}\n```' }
|
|
88
|
+
}
|
|
89
|
+
return { text: '```openchamber-message\n{"text":"我来保存这个默认提示词。"}\n```\n```openchamber-tool\n{"name":"update_default_prompt","arguments":{"prompt":"简洁回复"}}\n```' }
|
|
90
|
+
})
|
|
91
|
+
const onBubbleDelta = vi.fn()
|
|
92
|
+
const result = await runContactTurn({
|
|
93
|
+
assistant: { providerID: 'p', modelID: 'm' }, history: [], userText: '保存默认提示词',
|
|
94
|
+
tools, createChatCompletion: completion, language: 'zh-CN', onBubbleDelta,
|
|
95
|
+
})
|
|
96
|
+
expect(completion).toHaveBeenCalledTimes(2)
|
|
97
|
+
expect(result.bubbles).toEqual(['我来保存这个默认提示词。', '提示词已保存,接下来的对话会按这个设置回复。'])
|
|
98
|
+
expect(onBubbleDelta.mock.calls.map((call) => call[1])).toEqual(result.bubbles)
|
|
99
|
+
expect(result.text).not.toContain('Default prompt saved.')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('preserves a published message on abort and suppresses later public records and tools', async () => {
|
|
103
|
+
const controller = new AbortController()
|
|
104
|
+
const onBubbleDelta = vi.fn()
|
|
105
|
+
const prefix = '```openchamber-message\n{"text":"我先确认设置。"}\n```\n'
|
|
106
|
+
const stream = createContactStreamFn(async ({ onTextDelta }) => {
|
|
107
|
+
onTextDelta(prefix)
|
|
108
|
+
controller.abort()
|
|
109
|
+
onTextDelta('```openchamber-message\n{"text":"这条迟到消息应丢弃。"}\n```')
|
|
110
|
+
return { text: prefix + '```openchamber-tool\n{"name":"write","arguments":{"path":"x","content":"y"}}\n```' }
|
|
111
|
+
}, { onBubbleDelta, signal: controller.signal })(
|
|
112
|
+
{ name: 'p/m', id: 'm', provider: 'openchamber', api: 'openai-completions' },
|
|
113
|
+
{ messages: [], tools: [{ name: 'write' }] },
|
|
114
|
+
)
|
|
115
|
+
expect((await stream.result()).stopReason).toBe('aborted')
|
|
116
|
+
expect(onBubbleDelta.mock.calls).toEqual([[0, '我先确认设置。', true]])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('recovers an incomplete streamed message from the final response without duplicate delivery', async () => {
|
|
120
|
+
const onBubbleDelta = vi.fn()
|
|
121
|
+
const prefix = '```openchamber-message\n{"text":"先确认一下。"}\n```\n'
|
|
122
|
+
const stream = createContactStreamFn(async ({ onTextDelta }) => {
|
|
123
|
+
onTextDelta(prefix.slice(0, 30))
|
|
124
|
+
return { text: prefix + '结果已确认。' }
|
|
125
|
+
}, { onBubbleDelta })(
|
|
126
|
+
{ name: 'p/m', id: 'm', provider: 'openchamber', api: 'openai-completions' }, { messages: [] },
|
|
127
|
+
)
|
|
128
|
+
expect((await stream.result()).stopReason).toBe('stop')
|
|
129
|
+
expect(onBubbleDelta.mock.calls).toEqual([[0, '先确认一下。', true], [1, '结果已确认。', true]])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('preserves delivered public messages across a protocol correction without replaying rejected prose', async () => {
|
|
133
|
+
const onBubbleDelta = vi.fn()
|
|
134
|
+
const completion = vi.fn()
|
|
135
|
+
.mockResolvedValueOnce({ text: '```openchamber-message\n{"text":"我先检查配置。"}\n```\nI need to plan privately.' })
|
|
136
|
+
.mockResolvedValueOnce({ text: '```openchamber-final\n{"status":"blocked","text":"请提供配置文件位置。"}\n```' })
|
|
137
|
+
const stream = createContactStreamFn(completion, { onBubbleDelta })(
|
|
138
|
+
{ name: 'p/m', id: 'm', provider: 'openchamber', api: 'openai-completions' },
|
|
139
|
+
{ messages: [], tools: [{ name: 'read' }] },
|
|
140
|
+
)
|
|
141
|
+
const result = await stream.result()
|
|
142
|
+
expect(projectStreamedContactTurnBubbles([result])).toEqual(['我先检查配置。', '请提供配置文件位置。'])
|
|
143
|
+
expect(onBubbleDelta.mock.calls).toEqual([[0, '我先检查配置。', true], [1, '请提供配置文件位置。', true]])
|
|
144
|
+
expect(completion.mock.calls[1][0].body.messages).toContainEqual({ role: 'assistant', content: '我先检查配置。' })
|
|
145
|
+
expect(JSON.stringify(completion.mock.calls[1][0].body)).not.toContain('I need to plan privately.')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it.each(['{"text":42}', '{"text":""}', '{"text":"unterminated'])('rejects malformed public messages without exposing their records: %s', async (json) => {
|
|
149
|
+
const onBubbleDelta = vi.fn()
|
|
150
|
+
const text = '```openchamber-message\n' + json + '\n```'
|
|
151
|
+
const stream = createContactStreamFn(async ({ onTextDelta }) => {
|
|
152
|
+
onTextDelta(text)
|
|
153
|
+
return { text }
|
|
154
|
+
}, { onBubbleDelta })(
|
|
155
|
+
{ name: 'p/m', id: 'm', provider: 'openchamber', api: 'openai-completions' }, { messages: [] },
|
|
156
|
+
)
|
|
157
|
+
expect((await stream.result()).stopReason).toBe('error')
|
|
158
|
+
expect(onBubbleDelta).not.toHaveBeenCalled()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('keeps a completed reset committed when its model summary fails, with no canned reply', async () => {
|
|
162
|
+
const clearContactMemory = vi.fn(async () => ({ reset: true, memoryCleared: true }))
|
|
163
|
+
const onBubbleDelta = vi.fn()
|
|
164
|
+
const createChatCompletion = vi.fn()
|
|
165
|
+
.mockResolvedValueOnce({ text: '```openchamber-tool\n{"name":"new_conversation","arguments":{}}\n```' })
|
|
166
|
+
.mockRejectedValueOnce(new Error('model unavailable'))
|
|
167
|
+
await expect(runContactTurn({
|
|
168
|
+
assistant: { providerID: 'p', modelID: 'm', variant: 'high' }, history: [], userText: '清除记忆',
|
|
169
|
+
tools: createContactTools({ clearContactMemory }), createChatCompletion, onBubbleDelta,
|
|
170
|
+
})).rejects.toThrow('model unavailable')
|
|
171
|
+
expect(clearContactMemory).toHaveBeenCalledTimes(1)
|
|
172
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(2)
|
|
173
|
+
expect(createChatCompletion.mock.calls.map(([input]) => input.body.variant)).toEqual(['high', 'high'])
|
|
174
|
+
expect(onBubbleDelta).not.toHaveBeenCalled()
|
|
175
|
+
})
|
|
176
|
+
|
|
25
177
|
it('runs managed contact lookups from the user home with the coordination policy', async () => {
|
|
26
178
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-contact-home-'))
|
|
27
179
|
const homeSpy = vi.spyOn(os, 'homedir').mockReturnValue(home)
|
|
@@ -207,7 +359,7 @@ describe('createContactStreamFn', () => {
|
|
|
207
359
|
assistant: { providerID: 'openai', modelID: 'gpt-5.2', defaultPrompt: '' },
|
|
208
360
|
history: [],
|
|
209
361
|
userText: '监听这个会话',
|
|
210
|
-
createChatCompletion: vi.fn(),
|
|
362
|
+
createChatCompletion: vi.fn(async () => ({ text: '有结果会告诉你。' })),
|
|
211
363
|
tools: [{
|
|
212
364
|
name: 'watch_session',
|
|
213
365
|
execute: vi.fn(async () => ({
|
|
@@ -488,14 +640,14 @@ describe('runContactTurn', () => {
|
|
|
488
640
|
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-contact-batch-'))
|
|
489
641
|
const assignWork = vi.fn(async () => ({ sessionID: 'ses_once', directory: workspace, title: 'Work', status: 'busy' }))
|
|
490
642
|
const assign = '```openchamber-tool\n' + JSON.stringify({ name: 'assign_session', arguments: { projectPath: workspace, prompt: 'Work' } }) + '\n```'
|
|
491
|
-
const replies = [assign + '\n```openchamber-tool\n{"name":"write","arguments":{"path":"unexpected.txt","content":"bad"}}\n```', assign]
|
|
643
|
+
const replies = [assign + '\n```openchamber-tool\n{"name":"write","arguments":{"path":"unexpected.txt","content":"bad"}}\n```', assign, '任务已经交给工作会话。']
|
|
492
644
|
const createChatCompletion = vi.fn(async () => ({ text: replies.shift() || '' }))
|
|
493
645
|
try {
|
|
494
646
|
const result = await runContactTurn({
|
|
495
647
|
assistant: { providerID: 'p', modelID: 'm', effectiveWorkspacePath: workspace },
|
|
496
648
|
history: [], userText: '建个会话', skillHomeDir: path.join(workspace, 'home'), tools: createContactTools({ assignWork }), createChatCompletion,
|
|
497
649
|
})
|
|
498
|
-
expect(createChatCompletion).toHaveBeenCalledTimes(
|
|
650
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(3)
|
|
499
651
|
expect(assignWork).toHaveBeenCalledTimes(1)
|
|
500
652
|
expect(result.cards[0].sessionID).toBe('ses_once')
|
|
501
653
|
expect(fs.existsSync(path.join(workspace, 'unexpected.txt'))).toBe(false)
|
|
@@ -520,6 +672,7 @@ describe('runContactTurn', () => {
|
|
|
520
672
|
]
|
|
521
673
|
let call = 0
|
|
522
674
|
const createChatCompletion = vi.fn(async ({ body }) => {
|
|
675
|
+
if (call === 3) return { text: replies[call++] }
|
|
523
676
|
expect(body.messages[0].content).toContain('assign_session')
|
|
524
677
|
expect(body.messages[0].content).toContain('docs')
|
|
525
678
|
expect(body.messages[0].content).toContain('arguments schema:')
|
|
@@ -539,8 +692,8 @@ describe('runContactTurn', () => {
|
|
|
539
692
|
expect(result.tools.map((tool) => tool.name)).toEqual([...PI_CODING_TOOL_NAMES, ...tools.map((tool) => tool.name)])
|
|
540
693
|
expect(assignWork).toHaveBeenCalledTimes(1)
|
|
541
694
|
expect(result.cards).toEqual([expect.objectContaining({ sessionID: 'ses_mixed' })])
|
|
542
|
-
//
|
|
543
|
-
expect(createChatCompletion).toHaveBeenCalledTimes(
|
|
695
|
+
// Assignment terminates mutations; a tools-disabled completion summarizes the result.
|
|
696
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(4)
|
|
544
697
|
// Tool call/result association: after read, the next completion sees a named tool result.
|
|
545
698
|
const secondMessages = createChatCompletion.mock.calls[1][0].body.messages
|
|
546
699
|
const toolResultTurn = secondMessages.find((message) => (
|
|
@@ -561,8 +714,9 @@ describe('runContactTurn', () => {
|
|
|
561
714
|
}))
|
|
562
715
|
const tools = createContactTools({ assignWork })
|
|
563
716
|
let completions = 0
|
|
564
|
-
const createChatCompletion = vi.fn(async () => {
|
|
717
|
+
const createChatCompletion = vi.fn(async ({ body }) => {
|
|
565
718
|
completions += 1
|
|
719
|
+
if (body.messages[0].content.startsWith('Reply briefly')) return { text: '任务已交给工作会话。' }
|
|
566
720
|
// Safety: a non-terminating loop must not mint 37 sessions in tests.
|
|
567
721
|
if (completions > 8) {
|
|
568
722
|
throw new Error('harness safety stop: too many completions without terminate')
|
|
@@ -590,9 +744,9 @@ describe('runContactTurn', () => {
|
|
|
590
744
|
expect(result.cards).toHaveLength(1)
|
|
591
745
|
// Card is the user-facing confirm — English toolText must not leak into bubbles.
|
|
592
746
|
expect(result.bubbles.join('\n')).not.toContain('Opened a coding session.')
|
|
593
|
-
//
|
|
594
|
-
expect(createChatCompletion).toHaveBeenCalledTimes(
|
|
595
|
-
expect(completions).toBe(
|
|
747
|
+
// The result summary has no tools and cannot create another worker.
|
|
748
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(2)
|
|
749
|
+
expect(completions).toBe(2)
|
|
596
750
|
})
|
|
597
751
|
|
|
598
752
|
it('real Agent loop: list_projects then assign keeps prereq + one session (terminate after assign)', async () => {
|
|
@@ -607,6 +761,7 @@ describe('runContactTurn', () => {
|
|
|
607
761
|
let completions = 0
|
|
608
762
|
const createChatCompletion = vi.fn(async ({ body }) => {
|
|
609
763
|
completions += 1
|
|
764
|
+
if (body.messages[0].content.startsWith('Reply briefly')) return { text: '已找到项目并安排工作。' }
|
|
610
765
|
if (completions > 6) {
|
|
611
766
|
throw new Error('harness safety stop: too many completions')
|
|
612
767
|
}
|
|
@@ -678,11 +833,11 @@ describe('runContactTurn', () => {
|
|
|
678
833
|
const result = await runContactTurn({
|
|
679
834
|
assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
|
|
680
835
|
history: [], userText: '建会话', tools: createContactTools(),
|
|
681
|
-
createChatCompletion: vi.fn(), AgentImpl,
|
|
836
|
+
createChatCompletion: vi.fn(async () => ({ text: '已安排工作。' })), AgentImpl,
|
|
682
837
|
})
|
|
683
838
|
expect(prompts).toHaveLength(2)
|
|
684
839
|
// assign toolText is not a user bubble. Turn-boundary keeps pre-retry assistant prose.
|
|
685
|
-
expect(result.bubbles).toEqual(['Only document search is available.'])
|
|
840
|
+
expect(result.bubbles).toEqual(['Only document search is available.', '已安排工作。'])
|
|
686
841
|
expect(result.bubbles.join('\n')).not.toContain('Opened session.')
|
|
687
842
|
})
|
|
688
843
|
it('runs pi-agent-core with thinking off and no tools by default', async () => {
|
|
@@ -888,13 +1043,13 @@ describe('runContactTurn', () => {
|
|
|
888
1043
|
assistant: { providerID: 'openai', modelID: 'gpt-5.2', defaultPrompt: '' },
|
|
889
1044
|
history: [],
|
|
890
1045
|
userText: 'assign login',
|
|
891
|
-
createChatCompletion: vi.fn(),
|
|
1046
|
+
createChatCompletion: vi.fn(async () => ({ text: '任务已安排。' })),
|
|
892
1047
|
tools: [assignTool, { name: 'bash', execute: vi.fn() }],
|
|
893
1048
|
projects: [{ id: 'proj_yee', path: '/repo/sample-app', label: 'OpenChamber Yee' }],
|
|
894
1049
|
AgentImpl,
|
|
895
1050
|
})
|
|
896
1051
|
// Keep assistant spoken/confirm text; do not paint English assign toolText.
|
|
897
|
-
expect(result.bubbles).toEqual(['Opening that.'])
|
|
1052
|
+
expect(result.bubbles).toEqual(['Opening that.', '任务已安排。'])
|
|
898
1053
|
expect(result.bubbles.join('\n')).not.toContain('opened')
|
|
899
1054
|
expect(result.cards).toEqual([expect.objectContaining({ sessionID: 'ses_1', cardType: 'session' })])
|
|
900
1055
|
expect(result.thinkingLevel).toBe('off')
|
|
@@ -934,7 +1089,7 @@ describe('runContactTurn', () => {
|
|
|
934
1089
|
assistant: { providerID: 'openai', modelID: 'gpt-5.2', defaultPrompt: '' },
|
|
935
1090
|
history: [],
|
|
936
1091
|
userText: 'assign login',
|
|
937
|
-
createChatCompletion: vi.fn(),
|
|
1092
|
+
createChatCompletion: vi.fn(async () => ({ text: '已安排工作。' })),
|
|
938
1093
|
tools: [{ name: 'assign_session', execute: vi.fn() }],
|
|
939
1094
|
AgentImpl,
|
|
940
1095
|
})
|
|
@@ -943,7 +1098,7 @@ describe('runContactTurn', () => {
|
|
|
943
1098
|
expect(result.cards).toEqual([expect.objectContaining({ sessionID: 'ses_plan' })])
|
|
944
1099
|
})
|
|
945
1100
|
|
|
946
|
-
it('keeps a short spoken preamble then the
|
|
1101
|
+
it('keeps a short spoken preamble then the model summary', async () => {
|
|
947
1102
|
function AgentImpl(options) {
|
|
948
1103
|
this.state = { ...options.initialState, messages: [] }
|
|
949
1104
|
this.prompt = async () => {
|
|
@@ -977,11 +1132,11 @@ describe('runContactTurn', () => {
|
|
|
977
1132
|
assistant: { providerID: 'openai', modelID: 'gpt-5.2', defaultPrompt: '' },
|
|
978
1133
|
history: [],
|
|
979
1134
|
userText: 'assign login',
|
|
980
|
-
createChatCompletion: vi.fn(),
|
|
1135
|
+
createChatCompletion: vi.fn(async () => ({ text: '已安排工作。' })),
|
|
981
1136
|
tools: [{ name: 'assign_session', execute: vi.fn() }],
|
|
982
1137
|
AgentImpl,
|
|
983
1138
|
})
|
|
984
|
-
expect(result.bubbles).toEqual(['我去找一下'])
|
|
1139
|
+
expect(result.bubbles).toEqual(['我去找一下', '已安排工作。'])
|
|
985
1140
|
expect(result.cards).toEqual([expect.objectContaining({ sessionID: 'ses_speak' })])
|
|
986
1141
|
})
|
|
987
1142
|
|
|
@@ -1023,7 +1178,7 @@ describe('runContactTurn', () => {
|
|
|
1023
1178
|
expect(result.bubbles.join('\n')).not.toContain('<path')
|
|
1024
1179
|
})
|
|
1025
1180
|
|
|
1026
|
-
it('
|
|
1181
|
+
it('requests a model summary when read_session has no post-read reply', async () => {
|
|
1027
1182
|
function AgentImpl(options) {
|
|
1028
1183
|
this.state = { ...options.initialState, messages: [] }
|
|
1029
1184
|
this.prompt = async () => {
|
|
@@ -1047,11 +1202,11 @@ describe('runContactTurn', () => {
|
|
|
1047
1202
|
assistant: { providerID: 'openai', modelID: 'gpt-5.2', defaultPrompt: '' },
|
|
1048
1203
|
history: [],
|
|
1049
1204
|
userText: '@session:ses_quote 这个对话在说什么',
|
|
1050
|
-
createChatCompletion: vi.fn(),
|
|
1205
|
+
createChatCompletion: vi.fn(async () => ({ text: '这个会话目前为空。' })),
|
|
1051
1206
|
tools: [{ name: 'read_session', execute: vi.fn() }],
|
|
1052
1207
|
AgentImpl,
|
|
1053
1208
|
})
|
|
1054
|
-
expect(result.bubbles).toEqual(['我去读一下这个会话。'])
|
|
1209
|
+
expect(result.bubbles).toEqual(['我去读一下这个会话。', '这个会话目前为空。'])
|
|
1055
1210
|
expect(result.bubbles.join('\n')).not.toContain('Referenced conversation data')
|
|
1056
1211
|
})
|
|
1057
1212
|
|
|
@@ -1120,7 +1275,7 @@ describe('runContactTurn', () => {
|
|
|
1120
1275
|
assistant: { providerID: 'opencode-go', modelID: 'deepseek-v4-flash', defaultPrompt: '' },
|
|
1121
1276
|
history: [],
|
|
1122
1277
|
userText: '帮我新建一个助理,名叫 FlowNL,不要开编码 session',
|
|
1123
|
-
createChatCompletion: vi.fn(),
|
|
1278
|
+
createChatCompletion: vi.fn(async () => ({ text: 'FlowNL 已经建好了。' })),
|
|
1124
1279
|
tools,
|
|
1125
1280
|
AgentImpl,
|
|
1126
1281
|
})
|
|
@@ -1180,7 +1335,7 @@ describe('runContactTurn', () => {
|
|
|
1180
1335
|
expect(result.cards).toEqual([])
|
|
1181
1336
|
// Prefer existing assistant text over the English missed-tool fallback.
|
|
1182
1337
|
expect(result.bubbles).toEqual(['好的,已创建。', '好的,已创建。'])
|
|
1183
|
-
expect(result.
|
|
1338
|
+
expect(result.text).not.toContain('I could not complete that. No tool ran')
|
|
1184
1339
|
})
|
|
1185
1340
|
|
|
1186
1341
|
it('retries a missed fence once and then executes message_assistant', async () => {
|
|
@@ -1225,7 +1380,7 @@ describe('runContactTurn', () => {
|
|
|
1225
1380
|
assistant: { providerID: 'opencode-go', modelID: 'deepseek-v4-flash', defaultPrompt: '' },
|
|
1226
1381
|
history: [],
|
|
1227
1382
|
userText: '给 PeerQA 说一声 hello-from-assistant 写好了',
|
|
1228
|
-
createChatCompletion: vi.fn(),
|
|
1383
|
+
createChatCompletion: vi.fn(async () => ({ text: '已经告诉 PeerQA 了。' })),
|
|
1229
1384
|
tools,
|
|
1230
1385
|
AgentImpl,
|
|
1231
1386
|
})
|
|
@@ -1275,7 +1430,10 @@ describe('runContactTurn', () => {
|
|
|
1275
1430
|
{ role: 'assistant', content: 'got the image' },
|
|
1276
1431
|
],
|
|
1277
1432
|
userText: '开新对话',
|
|
1278
|
-
createChatCompletion: vi.fn()
|
|
1433
|
+
createChatCompletion: vi.fn(async ({ body }) => {
|
|
1434
|
+
expect(JSON.stringify(body)).not.toMatch(/dot.png|note.txt|got the image/)
|
|
1435
|
+
return { text: '记忆已清除,我们可以开始新话题了。' }
|
|
1436
|
+
}),
|
|
1279
1437
|
tools,
|
|
1280
1438
|
AgentImpl,
|
|
1281
1439
|
})
|
|
@@ -1284,18 +1442,22 @@ describe('runContactTurn', () => {
|
|
|
1284
1442
|
expect(result.reset).toBe(true)
|
|
1285
1443
|
expect(result.historyCleared).toBe(false)
|
|
1286
1444
|
expect(result.cards).toEqual([])
|
|
1287
|
-
expect(result.bubbles).toEqual([
|
|
1445
|
+
expect(result.bubbles).toEqual(['记忆已清除,我们可以开始新话题了。'])
|
|
1288
1446
|
expect(result.bubbles.join('')).not.toContain('dot.png')
|
|
1289
1447
|
expect(result.bubbles.join('')).not.toContain('note.txt')
|
|
1290
1448
|
})
|
|
1291
1449
|
|
|
1292
|
-
it('real Agent loop: clear_chat_history
|
|
1450
|
+
it('real Agent loop: clear_chat_history runs once and gets an isolated model summary', async () => {
|
|
1293
1451
|
const clearContactMemory = vi.fn(async () => ({ reset: true, memoryCleared: true }))
|
|
1294
1452
|
const resetContact = vi.fn(async () => ({ reset: true, historyCleared: true }))
|
|
1295
1453
|
const tools = createContactTools({ clearContactMemory, resetContact })
|
|
1296
1454
|
let completions = 0
|
|
1297
|
-
const createChatCompletion = vi.fn(async () => {
|
|
1455
|
+
const createChatCompletion = vi.fn(async ({ body }) => {
|
|
1298
1456
|
completions += 1
|
|
1457
|
+
if (completions === 2) {
|
|
1458
|
+
expect(JSON.stringify(body)).not.toContain('secret')
|
|
1459
|
+
return { text: '聊天记录已清空。' }
|
|
1460
|
+
}
|
|
1299
1461
|
if (completions > 4) {
|
|
1300
1462
|
throw new Error('harness safety stop: clear_chat_history must terminate without follow-up LLM')
|
|
1301
1463
|
}
|
|
@@ -1325,11 +1487,11 @@ describe('runContactTurn', () => {
|
|
|
1325
1487
|
expect(clearContactMemory).not.toHaveBeenCalled()
|
|
1326
1488
|
expect(result.reset).toBe(true)
|
|
1327
1489
|
expect(result.historyCleared).toBe(true)
|
|
1328
|
-
expect(result.bubbles).toEqual([
|
|
1490
|
+
expect(result.bubbles).toEqual(['聊天记录已清空。'])
|
|
1329
1491
|
expect(result.bubbles.join('')).not.toMatch(/not cleared|denied|new_conversation instead|secret/i)
|
|
1330
1492
|
expect(result.cards).toEqual([])
|
|
1331
|
-
expect(createChatCompletion).toHaveBeenCalledTimes(
|
|
1332
|
-
expect(completions).toBe(
|
|
1493
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(2)
|
|
1494
|
+
expect(completions).toBe(2)
|
|
1333
1495
|
})
|
|
1334
1496
|
|
|
1335
1497
|
it('real Agent loop: clear_chat_history after cross-turn confirm terminates once with confirm only', async () => {
|
|
@@ -1339,6 +1501,7 @@ describe('runContactTurn', () => {
|
|
|
1339
1501
|
let completions = 0
|
|
1340
1502
|
const createChatCompletion = vi.fn(async () => {
|
|
1341
1503
|
completions += 1
|
|
1504
|
+
if (completions === 2) return { text: '聊天记录已清空。' }
|
|
1342
1505
|
if (completions > 4) {
|
|
1343
1506
|
throw new Error('harness safety stop: wipe must terminate once')
|
|
1344
1507
|
}
|
|
@@ -1366,10 +1529,10 @@ describe('runContactTurn', () => {
|
|
|
1366
1529
|
expect(clearContactMemory).not.toHaveBeenCalled()
|
|
1367
1530
|
expect(result.reset).toBe(true)
|
|
1368
1531
|
expect(result.historyCleared).toBe(true)
|
|
1369
|
-
expect(result.bubbles).toEqual([
|
|
1532
|
+
expect(result.bubbles).toEqual(['聊天记录已清空。'])
|
|
1370
1533
|
expect(result.bubbles.join('')).not.toMatch(/not cleared|denied|explicit wipe|new_conversation instead/i)
|
|
1371
|
-
expect(createChatCompletion).toHaveBeenCalledTimes(
|
|
1372
|
-
expect(completions).toBe(
|
|
1534
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(2)
|
|
1535
|
+
expect(completions).toBe(2)
|
|
1373
1536
|
})
|
|
1374
1537
|
|
|
1375
1538
|
it('real Agent loop: new_conversation terminates once and does not call resetContact', async () => {
|
|
@@ -1379,6 +1542,7 @@ describe('runContactTurn', () => {
|
|
|
1379
1542
|
let completions = 0
|
|
1380
1543
|
const createChatCompletion = vi.fn(async () => {
|
|
1381
1544
|
completions += 1
|
|
1545
|
+
if (completions === 2) return { text: '记忆已清除,聊天记录保留。' }
|
|
1382
1546
|
if (completions > 4) {
|
|
1383
1547
|
throw new Error('harness safety stop: new_conversation must terminate without follow-up LLM')
|
|
1384
1548
|
}
|
|
@@ -1406,9 +1570,9 @@ describe('runContactTurn', () => {
|
|
|
1406
1570
|
expect(resetContact).not.toHaveBeenCalled()
|
|
1407
1571
|
expect(result.reset).toBe(true)
|
|
1408
1572
|
expect(result.historyCleared).toBe(false)
|
|
1409
|
-
expect(result.bubbles).toEqual([
|
|
1410
|
-
expect(createChatCompletion).toHaveBeenCalledTimes(
|
|
1411
|
-
expect(completions).toBe(
|
|
1573
|
+
expect(result.bubbles).toEqual(['记忆已清除,聊天记录保留。'])
|
|
1574
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(2)
|
|
1575
|
+
expect(completions).toBe(2)
|
|
1412
1576
|
})
|
|
1413
1577
|
})
|
|
1414
1578
|
|
|
@@ -1543,11 +1707,11 @@ describe('runContactTurn bubble index continuity', () => {
|
|
|
1543
1707
|
})
|
|
1544
1708
|
const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
|
|
1545
1709
|
expect(doneBubbles).toEqual(['我先看一下', '马上改人设', '改好了'])
|
|
1546
|
-
//
|
|
1710
|
+
// Only model-authored messages form the published prefix and final transcript.
|
|
1547
1711
|
expect(result.bubbles.slice(0, 3)).toEqual(['我先看一下', '马上改人设', '改好了'])
|
|
1548
|
-
expect(result.bubbles
|
|
1712
|
+
expect(result.bubbles).toHaveLength(3)
|
|
1549
1713
|
expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles)
|
|
1550
|
-
expect(result.bubbles.
|
|
1714
|
+
expect(result.bubbles.join('')).not.toMatch(/Default prompt/i)
|
|
1551
1715
|
expect(result.bubbles.join('\n')).not.toContain('openchamber-tool')
|
|
1552
1716
|
})
|
|
1553
1717
|
|
|
@@ -1582,7 +1746,7 @@ describe('runContactTurn bubble index continuity', () => {
|
|
|
1582
1746
|
expect(result.bubbles).toEqual(expect.arrayContaining(['好的,我来创建。']))
|
|
1583
1747
|
// Final array keeps the full turn projection (streamed prefix stable).
|
|
1584
1748
|
expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles)
|
|
1585
|
-
expect(result.
|
|
1749
|
+
expect(result.text).not.toContain('I could not complete that. No tool ran')
|
|
1586
1750
|
// Card tool: English tool confirm stays out of bubbles.
|
|
1587
1751
|
expect(result.bubbles.join('')).not.toContain('Created assistant')
|
|
1588
1752
|
})
|
|
@@ -53,15 +53,10 @@ import {
|
|
|
53
53
|
readContactAttachmentBytes,
|
|
54
54
|
} from './contact-attachments.js';
|
|
55
55
|
import {
|
|
56
|
-
ASSIGNED_SESSION_FALLBACK_BUBBLE,
|
|
57
56
|
boundSessionListLimit,
|
|
58
|
-
CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
59
|
-
confirmBubbleAfterContactReset,
|
|
60
|
-
contactTurnClearedChatHistory,
|
|
61
57
|
createContactTools,
|
|
62
58
|
filterRegisteredProjects,
|
|
63
59
|
matchesProjectQuery,
|
|
64
|
-
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
65
60
|
normalizeConnectedModels,
|
|
66
61
|
normalizeRegisteredProjects,
|
|
67
62
|
sanitizeRegisteredProject,
|
|
@@ -2231,10 +2226,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2231
2226
|
const assignedCards = [];
|
|
2232
2227
|
const spokenByIndex = new Map();
|
|
2233
2228
|
let contactResetThisTurn = false;
|
|
2234
|
-
|
|
2235
|
-
const markContactReset = (historyCleared = false) => {
|
|
2229
|
+
const markContactReset = () => {
|
|
2236
2230
|
contactResetThisTurn = true;
|
|
2237
|
-
if (historyCleared) contactHistoryClearedThisTurn = true;
|
|
2238
2231
|
spokenByIndex.clear();
|
|
2239
2232
|
assignedCards.length = 0;
|
|
2240
2233
|
};
|
|
@@ -2357,15 +2350,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2357
2350
|
return;
|
|
2358
2351
|
}
|
|
2359
2352
|
const resetThisTurn = contactResetThisTurn || generated?.reset === true;
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|| generated?.historyCleared === true;
|
|
2363
|
-
const preferredConfirm = historyClearedThisTurn
|
|
2364
|
-
? CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE
|
|
2365
|
-
: NEW_CONVERSATION_CONFIRM_BUBBLE;
|
|
2366
|
-
const bubbles = resetThisTurn
|
|
2367
|
-
? confirmBubbleAfterContactReset(generated?.bubbles, preferredConfirm)
|
|
2368
|
-
: (Array.isArray(generated?.bubbles) ? generated.bubbles.filter((item) => typeof item === 'string' && item.trim()) : []);
|
|
2353
|
+
const bubbles = resetThisTurn && generated?.reset !== true ? []
|
|
2354
|
+
: (Array.isArray(generated?.bubbles) ? generated.bubbles.filter((item) => typeof item === 'string' && item.trim()).slice(0, resetThisTurn ? 1 : undefined) : []);
|
|
2369
2355
|
const cards = resetThisTurn
|
|
2370
2356
|
? []
|
|
2371
2357
|
: [
|
|
@@ -2403,7 +2389,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2403
2389
|
// No user row for resume — only assistant bubbles/cards (userMessageID is id prefix).
|
|
2404
2390
|
persistContactAssistantReply(assistantID, {
|
|
2405
2391
|
userMessageID: messageID,
|
|
2406
|
-
bubbles
|
|
2392
|
+
bubbles,
|
|
2407
2393
|
cards,
|
|
2408
2394
|
turnID,
|
|
2409
2395
|
reset: resetThisTurn,
|
|
@@ -2420,7 +2406,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2420
2406
|
name: assistantSnapshot.name,
|
|
2421
2407
|
turnID,
|
|
2422
2408
|
status: 'complete',
|
|
2423
|
-
body: spoken.
|
|
2409
|
+
body: spoken.at(-1) || cards[0]?.title || '',
|
|
2424
2410
|
});
|
|
2425
2411
|
resolveContactTurnSettlement(messageID, { status: 'complete' });
|
|
2426
2412
|
} catch (error) {
|
|
@@ -2556,10 +2542,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2556
2542
|
const assignedCards = [];
|
|
2557
2543
|
const spokenByIndex = new Map();
|
|
2558
2544
|
let contactResetThisTurn = false;
|
|
2559
|
-
|
|
2560
|
-
const markContactReset = (historyCleared = false) => {
|
|
2545
|
+
const markContactReset = () => {
|
|
2561
2546
|
contactResetThisTurn = true;
|
|
2562
|
-
if (historyCleared) contactHistoryClearedThisTurn = true;
|
|
2563
2547
|
spokenByIndex.clear();
|
|
2564
2548
|
assignedCards.length = 0;
|
|
2565
2549
|
};
|
|
@@ -2707,15 +2691,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2707
2691
|
return;
|
|
2708
2692
|
}
|
|
2709
2693
|
const resetThisTurn = contactResetThisTurn || generated?.reset === true;
|
|
2710
|
-
const
|
|
2711
|
-
|
|
2712
|
-
|| generated?.historyCleared === true;
|
|
2713
|
-
const preferredConfirm = historyClearedThisTurn
|
|
2714
|
-
? CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE
|
|
2715
|
-
: NEW_CONVERSATION_CONFIRM_BUBBLE;
|
|
2716
|
-
const bubbles = resetThisTurn
|
|
2717
|
-
? confirmBubbleAfterContactReset(generated?.bubbles, preferredConfirm)
|
|
2718
|
-
: (Array.isArray(generated?.bubbles) ? generated.bubbles.filter((item) => typeof item === 'string' && item.trim()) : []);
|
|
2694
|
+
const bubbles = resetThisTurn && generated?.reset !== true ? []
|
|
2695
|
+
: (Array.isArray(generated?.bubbles) ? generated.bubbles.filter((item) => typeof item === 'string' && item.trim()).slice(0, resetThisTurn ? 1 : undefined) : []);
|
|
2719
2696
|
const cards = resetThisTurn
|
|
2720
2697
|
? []
|
|
2721
2698
|
: [
|
|
@@ -2766,7 +2743,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2766
2743
|
}
|
|
2767
2744
|
persistContactAssistantReply(row.assistant_id, {
|
|
2768
2745
|
userMessageID: messageID,
|
|
2769
|
-
bubbles
|
|
2746
|
+
bubbles,
|
|
2770
2747
|
cards,
|
|
2771
2748
|
turnID,
|
|
2772
2749
|
reset: resetThisTurn,
|
|
@@ -2784,7 +2761,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2784
2761
|
name: assistantSnapshot.name,
|
|
2785
2762
|
turnID,
|
|
2786
2763
|
status: 'complete',
|
|
2787
|
-
body: spoken.
|
|
2764
|
+
body: spoken.at(-1) || cards[0]?.title || '',
|
|
2788
2765
|
});
|
|
2789
2766
|
resolveContactTurnSettlement(messageID, { status: 'complete' });
|
|
2790
2767
|
} catch (error) {
|