@shawnstack/quickforge 1.7.5 → 1.7.6
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/AgentProfilesPage-Dl5Peqov.js +1 -0
- package/dist/assets/ChatPanelHost-D3987TUJ.js +307 -0
- package/dist/assets/CloudAccountSettingsPage-BG5Nn3bq.js +1 -0
- package/dist/assets/PluginsPage-BDKYfLna.js +1 -0
- package/dist/assets/ScheduledTasksPage--GAki3lB.js +2 -0
- package/dist/assets/SettingsWorkspacePage-DSy66Tuw.js +1696 -0
- package/dist/assets/ShareLinksSettingsPage-BQQMsvJo.js +1 -0
- package/dist/assets/SharedConversationPage-B9BUVCri.js +1 -0
- package/dist/assets/TerminalDock-CT1nYo7P.js +2 -0
- package/dist/assets/{WorkspaceInspector-D9kdnLDd.js → WorkspaceInspector-BnfDLf9a.js} +5 -5
- package/dist/assets/{dist-DLKC_odd.js → dist-yDHbi5ux.js} +1 -1
- package/dist/assets/{esm-Df4edDpj.js → esm-sdsK6gaB.js} +2 -2
- package/dist/assets/{icons-CXPP1fFT.js → icons-2D18YbSI.js} +1 -1
- package/dist/assets/index-1ei6nhs-.css +3 -0
- package/dist/assets/index-BivLuZvX.js +65 -0
- package/dist/assets/{mcp-servers-dialog-BUSPvaqI.js → mcp-servers-dialog-BEV4_TXC.js} +2 -2
- package/dist/assets/{logger-8REuLPGK.js → model-display-label-C6-0eDxF.js} +1 -1
- package/dist/assets/{monaco-D6OhXgjg.js → monaco-DvXErxp2.js} +1 -1
- package/dist/assets/{react-vendor-1KC5PyRx.js → react-vendor-3UaA-C9W.js} +1 -1
- package/dist/assets/{skills-dialog-BThnIUBs.js → skills-dialog-BTJ7mzk_.js} +3 -3
- package/dist/assets/{web-DoV8MSMn.js → web-B5qgZJRv.js} +1 -1
- package/dist/index.html +8 -8
- package/dist/sw.js +17 -0
- package/package.json +2 -1
- package/runtime-assets/agent/darwin-arm64/qf-agent +0 -0
- package/runtime-assets/agent/darwin-x64/qf-agent +0 -0
- package/runtime-assets/agent/linux-arm64/qf-agent +0 -0
- package/runtime-assets/agent/linux-x64/qf-agent +0 -0
- package/runtime-assets/agent/win32-x64/qf-agent.exe +0 -0
- package/server/access-policy.mjs +11 -0
- package/server/agent-manager.mjs +349 -48
- package/server/agent-profiles.mjs +27 -8
- package/server/auto-compaction.mjs +35 -8
- package/server/cloud/client.mjs +4 -7
- package/server/cloud/config.mjs +6 -5
- package/server/cloud/credential-store.mjs +10 -3
- package/server/cloud/identity.mjs +17 -60
- package/server/cloud/models.mjs +1 -1
- package/server/cloud/qf-agent-process.mjs +571 -0
- package/server/cloud/runtime.mjs +11 -5
- package/server/cloud/service-config.mjs +8 -4
- package/server/index.mjs +96 -37
- package/server/mcp/registry.mjs +8 -7
- package/server/message-converters.mjs +40 -19
- package/server/model-catalog.mjs +11 -4
- package/server/opencode-acp-agent.mjs +1151 -0
- package/server/public-api.mjs +11 -5
- package/server/routes/agent-profiles.mjs +6 -3
- package/server/routes/agent.mjs +43 -3
- package/server/routes/backup.mjs +5 -4
- package/server/routes/cloud.mjs +54 -35
- package/server/routes/shares.mjs +7 -8
- package/server/routes/storage.mjs +9 -24
- package/server/routes/system.mjs +7 -4
- package/server/utils/process-tree.mjs +78 -0
- package/dist/assets/AgentProfilesPage-XNjuqJs7.js +0 -1
- package/dist/assets/ChatPanelHost-CuF-PZpc.js +0 -292
- package/dist/assets/CloudAccountSettingsPage-GJSoi6sO.js +0 -1
- package/dist/assets/PluginsPage-BQgp0qN-.js +0 -1
- package/dist/assets/ScheduledTasksPage-DdekATzq.js +0 -2
- package/dist/assets/SettingsWorkspacePage-Cb54Ic91.js +0 -1639
- package/dist/assets/ShareLinksSettingsPage-C75WJpaF.js +0 -1
- package/dist/assets/SharedConversationPage-CjMtSQTW.js +0 -1
- package/dist/assets/TerminalDock-D4zJBJc4.js +0 -2
- package/dist/assets/index-_ZhK6bvp.js +0 -65
- package/dist/assets/index-iVV2bP_f.css +0 -3
package/server/agent-manager.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { loadSkillToolContext, abortRunningCommand } from './tools/index.mjs'
|
|
|
8
8
|
import { createSkillTools, globalMemoryTool, workspaceTools } from './tools/definitions.mjs'
|
|
9
9
|
import { createMcpToolDefinitions, isMcpToolName } from './mcp/registry.mjs'
|
|
10
10
|
import { createPluginToolDefinitions, isPluginToolName } from './plugins/registry.mjs'
|
|
11
|
+
import { createOpenCodeAcpAgent } from './opencode-acp-agent.mjs'
|
|
11
12
|
import {
|
|
12
13
|
composeSubagentSystemPrompt,
|
|
13
14
|
formatSubagentTask,
|
|
@@ -148,6 +149,10 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
async function rebuildSessionTools(session) {
|
|
152
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) {
|
|
153
|
+
session.agent.state.tools = []
|
|
154
|
+
return
|
|
155
|
+
}
|
|
151
156
|
const profileToolNames = Array.isArray(session.agentProfile?.allowedTools) ? session.agentProfile.allowedTools : null
|
|
152
157
|
session.agent.state.tools = await createServerTools(
|
|
153
158
|
session.projectId,
|
|
@@ -180,6 +185,25 @@ const agentSessions = new Map()
|
|
|
180
185
|
|
|
181
186
|
const AGENT_ACCESS_MODE_DEFAULT = 'default'
|
|
182
187
|
const AGENT_ACCESS_MODE_FULL_ACCESS = 'full-access'
|
|
188
|
+
const AGENT_HARNESS_QUICKFORGE = 'quickforge'
|
|
189
|
+
const AGENT_HARNESS_OPENCODE = 'opencode'
|
|
190
|
+
|
|
191
|
+
export function normalizeAgentHarness(value, fallback = AGENT_HARNESS_QUICKFORGE) {
|
|
192
|
+
if (value === AGENT_HARNESS_QUICKFORGE || value === AGENT_HARNESS_OPENCODE) return value
|
|
193
|
+
if (fallback !== value) return normalizeAgentHarness(fallback, AGENT_HARNESS_QUICKFORGE)
|
|
194
|
+
return AGENT_HARNESS_QUICKFORGE
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function validateAgentHarness(value) {
|
|
198
|
+
if (value === undefined || value === null || value === '') return AGENT_HARNESS_QUICKFORGE
|
|
199
|
+
if (value === 'claude-code') {
|
|
200
|
+
throw Object.assign(new Error('Claude Code Harness is not available yet.'), { statusCode: 400 })
|
|
201
|
+
}
|
|
202
|
+
if (value !== AGENT_HARNESS_QUICKFORGE && value !== AGENT_HARNESS_OPENCODE) {
|
|
203
|
+
throw Object.assign(new Error(`Unsupported Harness: ${value}`), { statusCode: 400 })
|
|
204
|
+
}
|
|
205
|
+
return value
|
|
206
|
+
}
|
|
183
207
|
|
|
184
208
|
function normalizeAccessMode(value, fallback = AGENT_ACCESS_MODE_DEFAULT) {
|
|
185
209
|
if (value === AGENT_ACCESS_MODE_DEFAULT || value === AGENT_ACCESS_MODE_FULL_ACCESS) return value
|
|
@@ -279,6 +303,58 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
|
|
|
279
303
|
})
|
|
280
304
|
}
|
|
281
305
|
|
|
306
|
+
function createAcpApprovalPromise(session, request) {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
let settled = false
|
|
309
|
+
const requestedAt = Date.now()
|
|
310
|
+
const expiresAt = requestedAt + APPROVAL_TIMEOUT_MS
|
|
311
|
+
const timeout = setTimeout(() => {
|
|
312
|
+
if (settled) return
|
|
313
|
+
settled = true
|
|
314
|
+
pendingApprovals.delete(request.toolCallId)
|
|
315
|
+
resolve({ outcome: { outcome: 'cancelled' } })
|
|
316
|
+
}, APPROVAL_TIMEOUT_MS)
|
|
317
|
+
|
|
318
|
+
const cleanup = () => {
|
|
319
|
+
clearTimeout(timeout)
|
|
320
|
+
if (settled) return
|
|
321
|
+
settled = true
|
|
322
|
+
pendingApprovals.delete(request.toolCallId)
|
|
323
|
+
}
|
|
324
|
+
const allowOption = request.options.find((option) => option.kind === 'allow_once' || option.kind === 'allow_always')
|
|
325
|
+
const rejectOption = request.options.find((option) => option.kind === 'reject_once' || option.kind === 'reject_always')
|
|
326
|
+
|
|
327
|
+
pendingApprovals.set(request.toolCallId, {
|
|
328
|
+
resolve: (approved) => {
|
|
329
|
+
cleanup()
|
|
330
|
+
const option = approved ? allowOption : rejectOption
|
|
331
|
+
resolve(option
|
|
332
|
+
? { outcome: { outcome: 'selected', optionId: option.optionId } }
|
|
333
|
+
: { outcome: { outcome: 'cancelled' } })
|
|
334
|
+
},
|
|
335
|
+
reject: (error) => {
|
|
336
|
+
cleanup()
|
|
337
|
+
reject(error)
|
|
338
|
+
},
|
|
339
|
+
sessionId: session.sessionId,
|
|
340
|
+
toolName: request.toolName,
|
|
341
|
+
args: request.args,
|
|
342
|
+
source: 'opencode',
|
|
343
|
+
requestedAt,
|
|
344
|
+
expiresAt,
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
emitSessionEvent(session, {
|
|
348
|
+
type: 'tool_approval_required',
|
|
349
|
+
sessionId: session.sessionId,
|
|
350
|
+
toolCallId: request.toolCallId,
|
|
351
|
+
toolName: request.toolName,
|
|
352
|
+
args: request.args,
|
|
353
|
+
source: 'opencode',
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
282
358
|
function createAutoCompactApprovalPromise(session, details = {}) {
|
|
283
359
|
if (!session) return Promise.resolve(false)
|
|
284
360
|
const approvalId = randomUUID()
|
|
@@ -377,6 +453,24 @@ function userTextMessage(text, details) {
|
|
|
377
453
|
}
|
|
378
454
|
}
|
|
379
455
|
|
|
456
|
+
function assistantErrorMessage(text, model) {
|
|
457
|
+
return {
|
|
458
|
+
...assistantTextMessage('', model),
|
|
459
|
+
stopReason: 'error',
|
|
460
|
+
errorMessage: text,
|
|
461
|
+
timestamp: Date.now(),
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function appendAssistantErrorMessageOnce(messages, errorMessage, model) {
|
|
466
|
+
const current = Array.isArray(messages) ? messages : []
|
|
467
|
+
const lastMessage = current[current.length - 1]
|
|
468
|
+
const errorAlreadyShown = lastMessage?.role === 'assistant'
|
|
469
|
+
&& lastMessage?.stopReason === 'error'
|
|
470
|
+
&& lastMessage?.errorMessage
|
|
471
|
+
return errorAlreadyShown ? current : [...current, assistantErrorMessage(errorMessage, model)]
|
|
472
|
+
}
|
|
473
|
+
|
|
380
474
|
function compactedSessionTitle(title) {
|
|
381
475
|
const base = typeof title === 'string' && title.trim() ? title.trim() : 'New chat'
|
|
382
476
|
if (base === 'New chat') return 'Compacted chat'
|
|
@@ -564,6 +658,9 @@ function finishManualSessionRun(session, status, errorMessage) {
|
|
|
564
658
|
}
|
|
565
659
|
|
|
566
660
|
async function summarySession(session, initialUserMessage, summaryOptions) {
|
|
661
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) {
|
|
662
|
+
throw Object.assign(new Error('OpenCode Harness does not support QuickForge summary derivation yet.'), { statusCode: 409 })
|
|
663
|
+
}
|
|
567
664
|
if (session.agent.state.isStreaming) {
|
|
568
665
|
session.agent.state.messages = [
|
|
569
666
|
...session.agent.state.messages,
|
|
@@ -657,6 +754,8 @@ async function summarySession(session, initialUserMessage, summaryOptions) {
|
|
|
657
754
|
scope: session.scope,
|
|
658
755
|
projectId: session.projectId,
|
|
659
756
|
accessMode: session.accessMode,
|
|
757
|
+
harness: session.harness,
|
|
758
|
+
sourceHarnessSessionId: session.harness === AGENT_HARNESS_OPENCODE ? session.agent.harnessSessionId : null,
|
|
660
759
|
yoloMode: session.yoloMode,
|
|
661
760
|
model: session.model,
|
|
662
761
|
modelRef: session.modelRef,
|
|
@@ -1562,12 +1661,17 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1562
1661
|
createdAt = new Date().toISOString(),
|
|
1563
1662
|
lastModified = null,
|
|
1564
1663
|
contextCompaction = null,
|
|
1664
|
+
harness: rawHarness,
|
|
1665
|
+
harnessSessionId = null,
|
|
1666
|
+
sourceHarnessSessionId = null,
|
|
1667
|
+
openCodeUsage = null,
|
|
1565
1668
|
agentProfile = null,
|
|
1566
1669
|
idleRetention = null,
|
|
1567
1670
|
stateVersion = 0,
|
|
1568
1671
|
} = config
|
|
1569
1672
|
const accessMode = normalizeAccessMode(rawAccessMode, yoloMode)
|
|
1570
1673
|
const resolvedYoloMode = yoloModeFromAccessMode(accessMode)
|
|
1674
|
+
const harness = normalizeAgentHarness(rawHarness)
|
|
1571
1675
|
|
|
1572
1676
|
// Resolve project context for tool calls. Project conversations resolve to
|
|
1573
1677
|
// their directory; global conversations (no projectId) and any fallback fall
|
|
@@ -1582,8 +1686,8 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1582
1686
|
}
|
|
1583
1687
|
projectContext ??= defaultGlobalWorkspaceContext()
|
|
1584
1688
|
|
|
1585
|
-
// Build system prompt
|
|
1586
|
-
const projectConfig = await readProjectConfig()
|
|
1689
|
+
// Build system prompt and tools only for the native QuickForge runtime.
|
|
1690
|
+
const projectConfig = harness === AGENT_HARNESS_QUICKFORGE ? await readProjectConfig() : { projects: [], globalSkills: [] }
|
|
1587
1691
|
const configuredProject = projectId
|
|
1588
1692
|
? projectConfig.projects.find((project) => project.id === projectId)
|
|
1589
1693
|
: null
|
|
@@ -1592,14 +1696,20 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1592
1696
|
projectSkillNames: configuredProject?.skills,
|
|
1593
1697
|
}
|
|
1594
1698
|
const profileSystemPrompt = agentProfileSystemPrompt(agentProfile)
|
|
1595
|
-
const resolvedSystemPrompt =
|
|
1699
|
+
const resolvedSystemPrompt = harness === AGENT_HARNESS_QUICKFORGE
|
|
1700
|
+
? systemPrompt ?? `${await buildSystemPrompt(projectId)}${profileSystemPrompt}`
|
|
1701
|
+
: ''
|
|
1702
|
+
if (harness === AGENT_HARNESS_OPENCODE && messages.length > 0 && !harnessSessionId && !sourceHarnessSessionId) {
|
|
1703
|
+
throw Object.assign(new Error('OpenCode history requires a persisted ACP session ID or an ACP fork source.'), { statusCode: 400 })
|
|
1704
|
+
}
|
|
1596
1705
|
|
|
1597
1706
|
let resolvedAgentProfile = agentProfile
|
|
1598
1707
|
|
|
1599
|
-
//
|
|
1708
|
+
// OpenCode owns its login, model, tools and context. QuickForge runtime keeps
|
|
1709
|
+
// the existing model resolution path unchanged.
|
|
1600
1710
|
let resolvedModel = model
|
|
1601
1711
|
let resolvedModelRef = modelRef
|
|
1602
|
-
if (!resolvedModel) {
|
|
1712
|
+
if (!resolvedModel && harness === AGENT_HARNESS_QUICKFORGE) {
|
|
1603
1713
|
// Try to load the active preference from storage. A stale/hidden implicit
|
|
1604
1714
|
// preference may fall back, while an explicit session binding never does.
|
|
1605
1715
|
try {
|
|
@@ -1620,7 +1730,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1620
1730
|
}
|
|
1621
1731
|
}
|
|
1622
1732
|
if (resolvedAgentProfile && !resolvedModel) throw new Error('No active model is configured for the agent session.')
|
|
1623
|
-
const resolvedBinding = resolvedModel
|
|
1733
|
+
const resolvedBinding = harness === AGENT_HARNESS_QUICKFORGE && resolvedModel
|
|
1624
1734
|
? (resolvedModelRef
|
|
1625
1735
|
? { model: resolvedModel, modelRef: resolvedModelRef }
|
|
1626
1736
|
: model
|
|
@@ -1632,33 +1742,36 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1632
1742
|
? resolveAgentProfileThinkingLevel(agentProfile, thinkingLevel, resolvedModel)
|
|
1633
1743
|
: thinkingLevel
|
|
1634
1744
|
|
|
1635
|
-
// Build
|
|
1745
|
+
// Build native tools only for QuickForge. OpenCode uses its own tools and
|
|
1746
|
+
// must not receive QuickForge MCP, Skills, Memory or workspace definitions.
|
|
1636
1747
|
const profileToolNames = Array.isArray(agentProfile?.allowedTools) ? agentProfile.allowedTools : null
|
|
1637
|
-
const tools =
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
agentProfile
|
|
1648
|
-
? {
|
|
1649
|
-
allowedToolNames: profileToolNames,
|
|
1650
|
-
includeSubagentTool: false,
|
|
1651
|
-
includeMcpTools: false,
|
|
1652
|
-
parentSessionId: sessionId,
|
|
1653
|
-
sessionId,
|
|
1654
|
-
scope,
|
|
1655
|
-
}
|
|
1656
|
-
: {
|
|
1657
|
-
parentSessionId: sessionId,
|
|
1658
|
-
sessionId,
|
|
1659
|
-
scope,
|
|
1748
|
+
const tools = harness === AGENT_HARNESS_QUICKFORGE
|
|
1749
|
+
? await createServerTools(
|
|
1750
|
+
projectId,
|
|
1751
|
+
projectContext,
|
|
1752
|
+
skillsContext,
|
|
1753
|
+
!!projectContext,
|
|
1754
|
+
(toolName) => {
|
|
1755
|
+
if (profileToolNames && !profileToolNames.includes(toolName)) return `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.`
|
|
1756
|
+
const session = agentSessions.get(sessionId)
|
|
1757
|
+
return session ? createCommandToolPermissions(session)(toolName) : null
|
|
1660
1758
|
},
|
|
1661
|
-
|
|
1759
|
+
agentProfile
|
|
1760
|
+
? {
|
|
1761
|
+
allowedToolNames: profileToolNames,
|
|
1762
|
+
includeSubagentTool: false,
|
|
1763
|
+
includeMcpTools: false,
|
|
1764
|
+
parentSessionId: sessionId,
|
|
1765
|
+
sessionId,
|
|
1766
|
+
scope,
|
|
1767
|
+
}
|
|
1768
|
+
: {
|
|
1769
|
+
parentSessionId: sessionId,
|
|
1770
|
+
sessionId,
|
|
1771
|
+
scope,
|
|
1772
|
+
},
|
|
1773
|
+
)
|
|
1774
|
+
: []
|
|
1662
1775
|
|
|
1663
1776
|
// Resolve API key
|
|
1664
1777
|
const getApiKey = async (provider) => {
|
|
@@ -1670,10 +1783,21 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1670
1783
|
}
|
|
1671
1784
|
}
|
|
1672
1785
|
|
|
1673
|
-
const initialMemoryEnabled = await isGlobalMemoryEnabled()
|
|
1786
|
+
const initialMemoryEnabled = harness === AGENT_HARNESS_QUICKFORGE ? await isGlobalMemoryEnabled() : false
|
|
1674
1787
|
const initialMemoryRevision = initialMemoryEnabled ? await getGlobalMemoryRevision() : null
|
|
1675
1788
|
let session
|
|
1676
|
-
const agent =
|
|
1789
|
+
const agent = harness === AGENT_HARNESS_OPENCODE
|
|
1790
|
+
? await createOpenCodeAcpAgent({
|
|
1791
|
+
sessionId,
|
|
1792
|
+
cwd: projectContext.workspaceRoot,
|
|
1793
|
+
messages,
|
|
1794
|
+
harnessSessionId,
|
|
1795
|
+
sourceHarnessSessionId,
|
|
1796
|
+
restoredUsage: openCodeUsage,
|
|
1797
|
+
logger,
|
|
1798
|
+
requestPermission: (request) => createAcpApprovalPromise(session, request),
|
|
1799
|
+
})
|
|
1800
|
+
: new Agent({
|
|
1677
1801
|
initialState: {
|
|
1678
1802
|
systemPrompt: resolvedSystemPrompt,
|
|
1679
1803
|
model: resolvedModel,
|
|
@@ -1729,6 +1853,8 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1729
1853
|
session = {
|
|
1730
1854
|
sessionId,
|
|
1731
1855
|
agent,
|
|
1856
|
+
harness,
|
|
1857
|
+
harnessSessionId: agent.harnessSessionId || null,
|
|
1732
1858
|
projectContext,
|
|
1733
1859
|
projectId,
|
|
1734
1860
|
source: typeof source === 'string' && source.trim() ? source.trim() : null,
|
|
@@ -1801,6 +1927,15 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1801
1927
|
: undefined
|
|
1802
1928
|
if (timedEvent.type === 'agent_end') {
|
|
1803
1929
|
markLatestAssistantProcessFinished(agent.state.messages)
|
|
1930
|
+
// Ensure a failed run is visible in the conversation. Most failures emit
|
|
1931
|
+
// an assistant error message through the agent loop (handleRunFailure),
|
|
1932
|
+
// but a few paths (e.g. concurrent-run rejection) only set
|
|
1933
|
+
// `state.errorMessage`. Append an error message so the user sees the
|
|
1934
|
+
// reason at the end of the transcript instead of only a toast.
|
|
1935
|
+
const runError = session.agent.state.errorMessage
|
|
1936
|
+
if (runError) {
|
|
1937
|
+
agent.state.messages = appendAssistantErrorMessageOnce(agent.state.messages, runError, session.model)
|
|
1938
|
+
}
|
|
1804
1939
|
}
|
|
1805
1940
|
const forwardEvent = timedEvent.type === 'agent_end'
|
|
1806
1941
|
? {
|
|
@@ -1816,6 +1951,12 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1816
1951
|
emitSessionEvent(session, forwardEvent)
|
|
1817
1952
|
}
|
|
1818
1953
|
|
|
1954
|
+
// OpenCode runtime usage snapshots are authoritative and lightweight; the
|
|
1955
|
+
// debounced write keeps the latest usage durable without blocking the run.
|
|
1956
|
+
if (event.type === 'acp_session_usage_update') {
|
|
1957
|
+
scheduleSessionPersist(session)
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1819
1960
|
// Track status
|
|
1820
1961
|
if (event.type === 'agent_start') {
|
|
1821
1962
|
session.abortEndEmitted = false
|
|
@@ -1848,7 +1989,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1848
1989
|
|
|
1849
1990
|
if (event.type === 'message_end') {
|
|
1850
1991
|
const isUserMessage = event.message?.role === 'user' || event.message?.role === 'user-with-attachments'
|
|
1851
|
-
const isInitialUserMessage = isUserMessage && session.agent.state.messages.length === 1
|
|
1992
|
+
const isInitialUserMessage = isUserMessage && (event.isInitialUserMessage === true || session.agent.state.messages.length === 1)
|
|
1852
1993
|
const requiresDurableCloudMessage = isUserMessage && isManagedCloudModel(session.model) && Boolean(logicalMessageId(event.message))
|
|
1853
1994
|
if (isInitialUserMessage || requiresDurableCloudMessage) {
|
|
1854
1995
|
// Persist every managed Cloud user message before the provider stream starts,
|
|
@@ -1860,7 +2001,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1860
2001
|
session.sessionCreatedEmitted = true
|
|
1861
2002
|
emitSessionEvent(session, { type: 'session_created', metadata })
|
|
1862
2003
|
}
|
|
1863
|
-
scheduleSessionTitleGeneration(session, event.message)
|
|
2004
|
+
if (session.harness === AGENT_HARNESS_QUICKFORGE) scheduleSessionTitleGeneration(session, event.message)
|
|
1864
2005
|
}
|
|
1865
2006
|
} catch (err) {
|
|
1866
2007
|
logger.error(`Failed to persist user message for session ${sessionId}:`, err, { sessionId })
|
|
@@ -1907,7 +2048,7 @@ function sessionLastModifiedFromMessages(messages, fallback) {
|
|
|
1907
2048
|
* Persist session data to storage.
|
|
1908
2049
|
*/
|
|
1909
2050
|
async function persistSessionUnlocked(session) {
|
|
1910
|
-
const { sessionId, agent, scope, projectId, source, channelId, channelName, title, titleSource, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, modelRef, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
|
|
2051
|
+
const { sessionId, agent, harness, harnessSessionId, scope, projectId, source, channelId, channelName, title, titleSource, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, modelRef, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
|
|
1911
2052
|
const messages = agent.state.messages
|
|
1912
2053
|
|
|
1913
2054
|
if (messages.length === 0) {
|
|
@@ -1929,6 +2070,9 @@ async function persistSessionUnlocked(session) {
|
|
|
1929
2070
|
id: sessionId,
|
|
1930
2071
|
title,
|
|
1931
2072
|
titleSource,
|
|
2073
|
+
harness,
|
|
2074
|
+
harnessSessionId: agent.harnessSessionId || harnessSessionId || undefined,
|
|
2075
|
+
openCodeUsage: harness === AGENT_HARNESS_OPENCODE && agent.state.acpSession?.usage ? agent.state.acpSession.usage : undefined,
|
|
1932
2076
|
model,
|
|
1933
2077
|
modelRef: modelRef || undefined,
|
|
1934
2078
|
thinkingLevel,
|
|
@@ -1990,6 +2134,8 @@ async function persistSessionUnlocked(session) {
|
|
|
1990
2134
|
messageCount: messages.length,
|
|
1991
2135
|
usage,
|
|
1992
2136
|
thinkingLevel,
|
|
2137
|
+
harness,
|
|
2138
|
+
harnessSessionId: agent.harnessSessionId || harnessSessionId || undefined,
|
|
1993
2139
|
accessMode,
|
|
1994
2140
|
yoloMode,
|
|
1995
2141
|
preview,
|
|
@@ -2128,6 +2274,9 @@ export async function rollbackSessionMessages(sessionId, rollbackMessageIndex) {
|
|
|
2128
2274
|
errorCode: 'GENERATION_STILL_RUNNING_BEFORE_ROLLBACK',
|
|
2129
2275
|
})
|
|
2130
2276
|
}
|
|
2277
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) {
|
|
2278
|
+
throw Object.assign(new Error('OpenCode does not support rollback from a message position. This action is unavailable for OpenCode conversations.'), { statusCode: 409 })
|
|
2279
|
+
}
|
|
2131
2280
|
|
|
2132
2281
|
const messages = Array.isArray(session.agent.state.messages) ? session.agent.state.messages : []
|
|
2133
2282
|
const rollbackIndex = rollbackStartIndexFromMessage(messages, rollbackMessageIndex)
|
|
@@ -2137,7 +2286,14 @@ export async function rollbackSessionMessages(sessionId, rollbackMessageIndex) {
|
|
|
2137
2286
|
|
|
2138
2287
|
const nextMessages = messages.slice(0, rollbackIndex)
|
|
2139
2288
|
updateSessionMessages(session, nextMessages)
|
|
2140
|
-
|
|
2289
|
+
const compactedUpToIndex = Number(session.contextCompaction?.compactedUpToIndex) || 0
|
|
2290
|
+
if (rollbackIndex < compactedUpToIndex) {
|
|
2291
|
+
// 撤回越过压缩点,摘要覆盖的历史被截断,压缩失效
|
|
2292
|
+
resetSessionCompaction(session)
|
|
2293
|
+
} else {
|
|
2294
|
+
// 撤回发生在压缩点之后,摘要仍然有效,保留压缩上下文
|
|
2295
|
+
session.lastTransformedContextMessages = null
|
|
2296
|
+
}
|
|
2141
2297
|
session.status = 'idle'
|
|
2142
2298
|
session.finishedAt = new Date().toISOString()
|
|
2143
2299
|
await persistSession(session)
|
|
@@ -2219,7 +2375,7 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
|
|
|
2219
2375
|
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
2220
2376
|
}
|
|
2221
2377
|
if (modelAccessContext) session.modelAccessContext = modelAccessContext
|
|
2222
|
-
await refreshSessionModelBinding(session)
|
|
2378
|
+
if (session.harness === AGENT_HARNESS_QUICKFORGE) await refreshSessionModelBinding(session)
|
|
2223
2379
|
|
|
2224
2380
|
if (session.agent.state.isStreaming || session.abortPending) {
|
|
2225
2381
|
throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), {
|
|
@@ -2228,16 +2384,22 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
|
|
|
2228
2384
|
})
|
|
2229
2385
|
}
|
|
2230
2386
|
|
|
2231
|
-
await refreshMemoryState(session)
|
|
2387
|
+
if (session.harness === AGENT_HARNESS_QUICKFORGE) await refreshMemoryState(session)
|
|
2232
2388
|
resetIdleTimer(session)
|
|
2233
2389
|
|
|
2234
2390
|
// Build user message
|
|
2235
2391
|
const initialUserMessage = typeof message === 'string'
|
|
2236
2392
|
? { role: 'user', content: message, timestamp: new Date().toISOString() }
|
|
2237
2393
|
: message
|
|
2238
|
-
const commandState =
|
|
2394
|
+
const commandState = session.harness === AGENT_HARNESS_QUICKFORGE
|
|
2395
|
+
? await resolveCommandState(session, initialUserMessage, promptCommand)
|
|
2396
|
+
: { userMessage: initialUserMessage }
|
|
2239
2397
|
const resolvedUserMessage = commandState.userMessage ?? initialUserMessage
|
|
2240
2398
|
|
|
2399
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) {
|
|
2400
|
+
session.agent.validatePrompt(resolvedUserMessage)
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2241
2403
|
if (commandState.textResponse) {
|
|
2242
2404
|
session.agent.state.messages = [
|
|
2243
2405
|
...session.agent.state.messages,
|
|
@@ -2263,7 +2425,9 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
|
|
|
2263
2425
|
return compactSession(session, initialUserMessage, commandState.compact)
|
|
2264
2426
|
}
|
|
2265
2427
|
|
|
2266
|
-
const userMessage =
|
|
2428
|
+
const userMessage = session.harness === AGENT_HARNESS_QUICKFORGE
|
|
2429
|
+
? prepareCloudUserMessage(session, resolvedUserMessage)
|
|
2430
|
+
: resolvedUserMessage
|
|
2267
2431
|
|
|
2268
2432
|
// Set a meaningful fallback immediately. The AI title request starts only
|
|
2269
2433
|
// after the first user message has been persisted by the message_end handler.
|
|
@@ -2286,11 +2450,26 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
|
|
|
2286
2450
|
// Fire and forget — events come through eventBus
|
|
2287
2451
|
session.agent.prompt(userMessage).catch((err) => {
|
|
2288
2452
|
logger.error(`Agent prompt error for session ${sessionId}:`, err, { sessionId })
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2453
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) return
|
|
2454
|
+
const errorMessage = err.message || 'Unknown error'
|
|
2455
|
+
// Surface the failure at the end of the conversation itself so the user
|
|
2456
|
+
// sees the reason in the transcript, not only via toast/notification.
|
|
2457
|
+
session.agent.state.messages = appendAssistantErrorMessageOnce(
|
|
2458
|
+
session.agent.state.messages,
|
|
2459
|
+
errorMessage,
|
|
2460
|
+
session.model,
|
|
2461
|
+
)
|
|
2462
|
+
session.agent.state.errorMessage = errorMessage
|
|
2463
|
+
session.agent.state.isStreaming = false
|
|
2464
|
+
session.status = 'error'
|
|
2465
|
+
session.finishedAt = new Date().toISOString()
|
|
2466
|
+
const messages = session.agent.state.messages
|
|
2467
|
+
emitSessionEvent(session, { type: 'message_end', messages })
|
|
2468
|
+
emitSessionEvent(session, { type: 'error', error: errorMessage })
|
|
2469
|
+
emitSessionEvent(session, { type: 'agent_end', messages, errorMessage, status: 'error' })
|
|
2470
|
+
flushSessionPersist(session).catch((persistErr) =>
|
|
2471
|
+
logger.error(`Failed to persist session ${sessionId} after prompt error:`, persistErr, { sessionId }),
|
|
2472
|
+
)
|
|
2294
2473
|
}).finally(() => {
|
|
2295
2474
|
session.activeCommandName = null
|
|
2296
2475
|
session.activeCommandPermissions = null
|
|
@@ -2321,6 +2500,9 @@ export async function continueSession(sessionId, modelAccessContext = null) {
|
|
|
2321
2500
|
errorCode: 'GENERATION_ALREADY_RUNNING',
|
|
2322
2501
|
})
|
|
2323
2502
|
}
|
|
2503
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) {
|
|
2504
|
+
throw Object.assign(new Error('OpenCode does not support retry from a message position. This action is unavailable for OpenCode conversations.'), { statusCode: 409 })
|
|
2505
|
+
}
|
|
2324
2506
|
if (modelAccessContext) session.modelAccessContext = modelAccessContext
|
|
2325
2507
|
await refreshSessionModelBinding(session)
|
|
2326
2508
|
|
|
@@ -2343,7 +2525,14 @@ export async function continueSession(sessionId, modelAccessContext = null) {
|
|
|
2343
2525
|
const continuedUserMessage = prepareCloudUserMessage(session, commandState.userMessage ?? lastUserMessage)
|
|
2344
2526
|
const trimmedMessages = messages.slice(0, lastUserIndex).concat(continuedUserMessage)
|
|
2345
2527
|
updateSessionMessages(session, trimmedMessages)
|
|
2346
|
-
|
|
2528
|
+
const compactedUpToIndex = Number(session.contextCompaction?.compactedUpToIndex) || 0
|
|
2529
|
+
if (lastUserIndex < compactedUpToIndex) {
|
|
2530
|
+
// 重试点越过压缩点,摘要覆盖的历史被截断,压缩失效
|
|
2531
|
+
resetSessionCompaction(session)
|
|
2532
|
+
} else {
|
|
2533
|
+
// 重试点位于压缩点之后,摘要仍然有效,保留压缩上下文
|
|
2534
|
+
session.lastTransformedContextMessages = null
|
|
2535
|
+
}
|
|
2347
2536
|
if (isManagedCloudModel(session.model) && logicalMessageId(continuedUserMessage)) {
|
|
2348
2537
|
await flushSessionPersist(session)
|
|
2349
2538
|
}
|
|
@@ -2536,6 +2725,8 @@ export function getSessionState(sessionId) {
|
|
|
2536
2725
|
source: session.source || undefined,
|
|
2537
2726
|
channelId: session.channelId || undefined,
|
|
2538
2727
|
channelName: session.channelName || undefined,
|
|
2728
|
+
harness: session.harness,
|
|
2729
|
+
harnessSessionId: session.agent.harnessSessionId || session.harnessSessionId || undefined,
|
|
2539
2730
|
accessMode: session.accessMode,
|
|
2540
2731
|
yoloMode: session.yoloMode,
|
|
2541
2732
|
systemPrompt: session.agent.state.systemPrompt,
|
|
@@ -2557,6 +2748,7 @@ export function getSessionState(sessionId) {
|
|
|
2557
2748
|
contextUsage: getSessionContextUsage(session),
|
|
2558
2749
|
pendingToolApproval: getPendingApprovalForSession(session.sessionId),
|
|
2559
2750
|
pendingAutoCompactApproval: getPendingAutoCompactApprovalForSession(session.sessionId),
|
|
2751
|
+
acpSession: session.harness === AGENT_HARNESS_OPENCODE ? session.agent.state.acpSession : undefined,
|
|
2560
2752
|
isStreaming: session.abortPending ? false : session.agent.state.isStreaming,
|
|
2561
2753
|
errorMessage: session.agent.state.errorMessage,
|
|
2562
2754
|
}
|
|
@@ -2575,6 +2767,8 @@ export function getSessionStatus(sessionId) {
|
|
|
2575
2767
|
sessionId: session.sessionId,
|
|
2576
2768
|
scope: session.scope,
|
|
2577
2769
|
projectId: session.projectId,
|
|
2770
|
+
harness: session.harness,
|
|
2771
|
+
harnessSessionId: session.agent.harnessSessionId || session.harnessSessionId || undefined,
|
|
2578
2772
|
source: session.source || undefined,
|
|
2579
2773
|
channelId: session.channelId || undefined,
|
|
2580
2774
|
channelName: session.channelName || undefined,
|
|
@@ -2646,6 +2840,7 @@ export async function destroyAgent(sessionId) {
|
|
|
2646
2840
|
|
|
2647
2841
|
try {
|
|
2648
2842
|
session.agent.abort()
|
|
2843
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) await session.agent.dispose?.()
|
|
2649
2844
|
} catch {
|
|
2650
2845
|
// ignore
|
|
2651
2846
|
}
|
|
@@ -2704,11 +2899,15 @@ export async function restoreAgent(sessionId) {
|
|
|
2704
2899
|
createdAt: sessionData.createdAt,
|
|
2705
2900
|
lastModified: sessionData.lastModified,
|
|
2706
2901
|
contextCompaction: sessionData.contextCompaction || null,
|
|
2902
|
+
harness: normalizeAgentHarness(sessionData.harness),
|
|
2903
|
+
harnessSessionId: typeof sessionData.harnessSessionId === 'string' ? sessionData.harnessSessionId : null,
|
|
2904
|
+
openCodeUsage: sessionData.openCodeUsage || null,
|
|
2707
2905
|
idleRetention: sessionData.idleRetention || null,
|
|
2708
2906
|
stateVersion: sessionData.stateVersion,
|
|
2709
2907
|
})
|
|
2710
2908
|
} catch (err) {
|
|
2711
2909
|
logger.error(`Failed to restore agent ${sessionId}:`, err, { sessionId })
|
|
2910
|
+
if (err?.statusCode === 503 || err?.errorCode === 'OPENCODE_UNAVAILABLE') throw err
|
|
2712
2911
|
return null
|
|
2713
2912
|
}
|
|
2714
2913
|
}
|
|
@@ -2778,6 +2977,8 @@ export function listSessions() {
|
|
|
2778
2977
|
scope: session.scope,
|
|
2779
2978
|
status: session.status,
|
|
2780
2979
|
title: session.title,
|
|
2980
|
+
harness: session.harness,
|
|
2981
|
+
harnessSessionId: session.agent.harnessSessionId || session.harnessSessionId || undefined,
|
|
2781
2982
|
source: session.source || undefined,
|
|
2782
2983
|
channelId: session.channelId || undefined,
|
|
2783
2984
|
channelName: session.channelName || undefined,
|
|
@@ -2833,7 +3034,7 @@ export async function updateSessionAccessMode(sessionId, accessMode) {
|
|
|
2833
3034
|
|
|
2834
3035
|
session.accessMode = normalizeAccessMode(accessMode, session.accessMode)
|
|
2835
3036
|
session.yoloMode = yoloModeFromAccessMode(session.accessMode)
|
|
2836
|
-
await rebuildSessionTools(session)
|
|
3037
|
+
if (session.harness === AGENT_HARNESS_QUICKFORGE) await rebuildSessionTools(session)
|
|
2837
3038
|
await persistSession(session)
|
|
2838
3039
|
|
|
2839
3040
|
const state = getSessionState(sessionId)
|
|
@@ -2846,6 +3047,100 @@ export async function updateSessionYoloMode(sessionId, yoloMode) {
|
|
|
2846
3047
|
return updateSessionAccessMode(sessionId, yoloMode ? AGENT_ACCESS_MODE_FULL_ACCESS : AGENT_ACCESS_MODE_DEFAULT)
|
|
2847
3048
|
}
|
|
2848
3049
|
|
|
3050
|
+
function requireOpenCodeHarnessSession(sessionId) {
|
|
3051
|
+
const session = agentSessions.get(sessionId)
|
|
3052
|
+
if (!session) throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
3053
|
+
if (session.harness !== AGENT_HARNESS_OPENCODE) {
|
|
3054
|
+
throw Object.assign(new Error('Harness configuration is only available for OpenCode sessions.'), { statusCode: 409 })
|
|
3055
|
+
}
|
|
3056
|
+
return session
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
export async function updateSessionHarnessConfigOption(sessionId, configId, value) {
|
|
3060
|
+
const session = requireOpenCodeHarnessSession(sessionId)
|
|
3061
|
+
await session.agent.setConfigOption(configId, value)
|
|
3062
|
+
const state = getSessionState(sessionId)
|
|
3063
|
+
emitSessionEvent(session, { type: 'state', ...state })
|
|
3064
|
+
return { sessionId, acpSession: state.acpSession }
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
export async function updateSessionHarnessMode(sessionId, modeId) {
|
|
3068
|
+
const session = requireOpenCodeHarnessSession(sessionId)
|
|
3069
|
+
await session.agent.setMode(modeId)
|
|
3070
|
+
const state = getSessionState(sessionId)
|
|
3071
|
+
emitSessionEvent(session, { type: 'state', ...state })
|
|
3072
|
+
return { sessionId, acpSession: state.acpSession }
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
/**
|
|
3076
|
+
* Fork the entire current OpenCode session into a new QuickForge session.
|
|
3077
|
+
*
|
|
3078
|
+
* OpenCode ACP only supports whole-session `session/fork` (no message-position
|
|
3079
|
+
* fork), so the new agent is created with the full message history and
|
|
3080
|
+
* `sourceHarnessSessionId` pointing at the current ACP session. The new session
|
|
3081
|
+
* is persisted immediately and announced through the existing `session_forked`
|
|
3082
|
+
* event so clients switch to it without a message-level fork semantic.
|
|
3083
|
+
*/
|
|
3084
|
+
export async function forkSession(sessionId) {
|
|
3085
|
+
const session = requireOpenCodeHarnessSession(sessionId)
|
|
3086
|
+
if (session.agent.state.isStreaming) {
|
|
3087
|
+
throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes before forking the conversation.'), {
|
|
3088
|
+
statusCode: 409,
|
|
3089
|
+
errorCode: 'GENERATION_STILL_RUNNING_BEFORE_FORK',
|
|
3090
|
+
})
|
|
3091
|
+
}
|
|
3092
|
+
const sourceHarnessSessionId = session.agent.harnessSessionId
|
|
3093
|
+
if (!sourceHarnessSessionId) {
|
|
3094
|
+
throw Object.assign(new Error('This OpenCode conversation has no ACP session to fork.'), { statusCode: 409 })
|
|
3095
|
+
}
|
|
3096
|
+
const messages = session.agent.state.messages
|
|
3097
|
+
if (messages.length === 0) {
|
|
3098
|
+
throw Object.assign(new Error('There is no conversation to fork yet.'), { statusCode: 400 })
|
|
3099
|
+
}
|
|
3100
|
+
const forkedSessionId = randomUUID()
|
|
3101
|
+
const forkedSession = await createAgent(forkedSessionId, {
|
|
3102
|
+
scope: session.scope,
|
|
3103
|
+
projectId: session.projectId,
|
|
3104
|
+
accessMode: session.accessMode,
|
|
3105
|
+
yoloMode: session.yoloMode,
|
|
3106
|
+
model: session.model,
|
|
3107
|
+
modelRef: session.modelRef,
|
|
3108
|
+
modelAccessContext: session.modelAccessContext,
|
|
3109
|
+
resolvePersistedModel: true,
|
|
3110
|
+
thinkingLevel: session.thinkingLevel,
|
|
3111
|
+
messages,
|
|
3112
|
+
title: session.title,
|
|
3113
|
+
titleSource: session.titleSource === 'default' ? 'manual' : session.titleSource,
|
|
3114
|
+
createdAt: new Date().toISOString(),
|
|
3115
|
+
harness: session.harness,
|
|
3116
|
+
sourceHarnessSessionId,
|
|
3117
|
+
idleRetention: session.idleRetention || null,
|
|
3118
|
+
})
|
|
3119
|
+
updateSessionMessages(forkedSession, messages)
|
|
3120
|
+
await persistSession(forkedSession)
|
|
3121
|
+
|
|
3122
|
+
emitSessionEvent(session, {
|
|
3123
|
+
type: 'session_forked',
|
|
3124
|
+
sourceSessionId: session.sessionId,
|
|
3125
|
+
targetSessionId: forkedSessionId,
|
|
3126
|
+
title: forkedSession.title,
|
|
3127
|
+
createdAt: forkedSession.createdAt,
|
|
3128
|
+
scope: forkedSession.scope,
|
|
3129
|
+
projectId: forkedSession.projectId,
|
|
3130
|
+
messages: forkedSession.agent.state.messages,
|
|
3131
|
+
})
|
|
3132
|
+
emitSessionEvent(forkedSession, { type: 'state', ...getSessionState(forkedSessionId) })
|
|
3133
|
+
emitSessionEvent(forkedSession, { type: 'message_end', messages: forkedSession.agent.state.messages })
|
|
3134
|
+
emitSessionEvent(forkedSession, { type: 'agent_end', messages: forkedSession.agent.state.messages })
|
|
3135
|
+
return {
|
|
3136
|
+
sessionId: forkedSessionId,
|
|
3137
|
+
title: forkedSession.title,
|
|
3138
|
+
createdAt: forkedSession.createdAt,
|
|
3139
|
+
scope: forkedSession.scope,
|
|
3140
|
+
projectId: forkedSession.projectId,
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
|
|
2849
3144
|
/**
|
|
2850
3145
|
* Update the model for an existing session.
|
|
2851
3146
|
* Syncs the model to both the session record (for persistence) and the agent state (for API calls).
|
|
@@ -2857,6 +3152,9 @@ export function updateSessionModel(sessionId, model, modelRef = null) {
|
|
|
2857
3152
|
if (!session) {
|
|
2858
3153
|
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
2859
3154
|
}
|
|
3155
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) {
|
|
3156
|
+
throw Object.assign(new Error('OpenCode manages its model natively.'), { statusCode: 409 })
|
|
3157
|
+
}
|
|
2860
3158
|
if (!model) {
|
|
2861
3159
|
throw Object.assign(new Error('Missing model'), { statusCode: 400 })
|
|
2862
3160
|
}
|
|
@@ -2877,6 +3175,9 @@ export function updateSessionThinkingLevel(sessionId, thinkingLevel) {
|
|
|
2877
3175
|
if (!session) {
|
|
2878
3176
|
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
2879
3177
|
}
|
|
3178
|
+
if (session.harness === AGENT_HARNESS_OPENCODE) {
|
|
3179
|
+
throw Object.assign(new Error('OpenCode manages its thinking level natively.'), { statusCode: 409 })
|
|
3180
|
+
}
|
|
2880
3181
|
if (!thinkingLevel) {
|
|
2881
3182
|
throw Object.assign(new Error('Missing thinkingLevel'), { statusCode: 400 })
|
|
2882
3183
|
}
|