@swarmclawai/swarmclaw 1.2.0 → 1.2.2
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/README.md +19 -0
- package/package.json +5 -2
- package/skills/coding-agent/SKILL.md +111 -0
- package/skills/github/SKILL.md +140 -0
- package/skills/nano-banana-pro/SKILL.md +62 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
- package/skills/nano-pdf/SKILL.md +53 -0
- package/skills/openai-image-gen/SKILL.md +78 -0
- package/skills/openai-image-gen/scripts/gen.py +328 -0
- package/skills/resourceful-problem-solving/SKILL.md +49 -0
- package/skills/skill-creator/SKILL.md +147 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/quick_validate.py +159 -0
- package/skills/summarize/SKILL.md +77 -0
- package/src/app/api/auth/route.ts +20 -5
- package/src/app/api/chats/[id]/deploy/route.ts +11 -6
- package/src/app/api/chats/[id]/devserver/route.ts +17 -20
- package/src/app/api/chats/[id]/messages/route.ts +15 -11
- package/src/app/api/chats/[id]/route.ts +9 -10
- package/src/app/api/chats/[id]/stop/route.ts +5 -7
- package/src/app/api/chats/messages-route.test.ts +8 -6
- package/src/app/api/chats/route.ts +9 -10
- package/src/app/api/credentials/[id]/route.ts +4 -1
- package/src/app/api/extensions/marketplace/route.ts +5 -2
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/memory/maintenance/route.ts +5 -2
- package/src/app/api/preview-server/route.ts +15 -12
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/app/api/system/status/route.ts +11 -0
- package/src/app/api/upload/route.ts +4 -1
- package/src/cli/index.js +7 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-files-editor.tsx +44 -32
- package/src/components/agents/personality-builder.tsx +13 -7
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/chat/chat-area.tsx +45 -23
- package/src/components/chat/message-bubble.test.ts +35 -0
- package/src/components/chat/message-bubble.tsx +20 -9
- package/src/components/chat/message-list.tsx +62 -42
- package/src/components/chat/swarm-status-card.tsx +10 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/layout/daemon-indicator.tsx +7 -8
- package/src/components/layout/update-banner.tsx +8 -13
- package/src/components/logs/log-list.tsx +1 -1
- package/src/components/memory/memory-card.tsx +3 -1
- package/src/components/org-chart/org-chart-view.tsx +4 -0
- package/src/components/projects/project-list.tsx +4 -2
- package/src/components/projects/tabs/overview-tab.tsx +3 -2
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +12 -6
- package/src/components/shared/dir-browser.tsx +22 -18
- package/src/components/skills/skill-sheet.tsx +2 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +1 -1
- package/src/hooks/use-openclaw-gateway.ts +46 -27
- package/src/instrumentation.ts +10 -7
- package/src/lib/chat/assistant-render-id.ts +3 -0
- package/src/lib/chat/chat-streaming-state.test.ts +42 -3
- package/src/lib/chat/chat-streaming-state.ts +20 -8
- package/src/lib/chat/chat.ts +18 -2
- package/src/lib/chat/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/anthropic.ts +6 -3
- package/src/lib/providers/claude-cli.ts +9 -3
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/providers/cli-utils.ts +15 -0
- package/src/lib/providers/codex-cli.ts +9 -3
- package/src/lib/providers/gemini-cli.ts +6 -2
- package/src/lib/providers/index.ts +4 -1
- package/src/lib/providers/ollama.ts +5 -2
- package/src/lib/providers/openai.ts +8 -5
- package/src/lib/providers/opencode-cli.ts +6 -2
- package/src/lib/server/activity/activity-log.ts +21 -0
- package/src/lib/server/agents/agent-availability.test.ts +10 -5
- package/src/lib/server/agents/agent-cascade.ts +79 -59
- package/src/lib/server/agents/agent-registry.ts +23 -4
- package/src/lib/server/agents/agent-repository.ts +90 -0
- package/src/lib/server/agents/delegation-job-repository.ts +53 -0
- package/src/lib/server/agents/delegation-jobs.ts +11 -4
- package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
- package/src/lib/server/agents/guardian.ts +2 -2
- package/src/lib/server/agents/main-agent-loop.ts +14 -6
- package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
- package/src/lib/server/agents/subagent-runtime.ts +9 -6
- package/src/lib/server/agents/subagent-swarm.ts +3 -2
- package/src/lib/server/agents/task-session.ts +3 -4
- package/src/lib/server/approvals/approval-repository.ts +30 -0
- package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
- package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
- package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution.ts +84 -1914
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
- package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/message-classifier.ts +5 -2
- package/src/lib/server/chat-execution/post-stream-finalization.ts +5 -2
- package/src/lib/server/chat-execution/prompt-builder.ts +22 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +55 -13
- package/src/lib/server/chat-execution/response-completeness.ts +5 -2
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +58 -25
- package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/bluebubbles.ts +7 -4
- package/src/lib/server/connectors/connector-inbound.ts +16 -13
- package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
- package/src/lib/server/connectors/connector-outbound.ts +6 -3
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/discord.ts +10 -7
- package/src/lib/server/connectors/email.ts +17 -14
- package/src/lib/server/connectors/googlechat.ts +7 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
- package/src/lib/server/connectors/matrix.ts +6 -3
- package/src/lib/server/connectors/openclaw.ts +20 -17
- package/src/lib/server/connectors/outbox.ts +4 -1
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/connectors/runtime-state.ts +19 -0
- package/src/lib/server/connectors/session-consolidation.ts +5 -2
- package/src/lib/server/connectors/signal.ts +9 -6
- package/src/lib/server/connectors/slack.ts +13 -10
- package/src/lib/server/connectors/teams.ts +8 -5
- package/src/lib/server/connectors/telegram.ts +15 -12
- package/src/lib/server/connectors/whatsapp.ts +32 -29
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/embeddings.ts +4 -1
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/link-understanding.ts +4 -1
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/memory/memory-abstract.ts +59 -0
- package/src/lib/server/memory/memory-db.ts +40 -14
- package/src/lib/server/missions/mission-repository.ts +74 -0
- package/src/lib/server/missions/mission-service/actions.ts +6 -0
- package/src/lib/server/missions/mission-service/bindings.ts +9 -0
- package/src/lib/server/missions/mission-service/context.ts +4 -0
- package/src/lib/server/missions/mission-service/core.ts +2269 -0
- package/src/lib/server/missions/mission-service/queries.ts +12 -0
- package/src/lib/server/missions/mission-service/recovery.ts +5 -0
- package/src/lib/server/missions/mission-service/ticks.ts +9 -0
- package/src/lib/server/missions/mission-service.test.ts +9 -2
- package/src/lib/server/missions/mission-service.ts +6 -2263
- package/src/lib/server/openclaw/gateway.ts +8 -5
- package/src/lib/server/persistence/repository-utils.ts +154 -0
- package/src/lib/server/persistence/storage-context.ts +51 -0
- package/src/lib/server/persistence/transaction.ts +1 -0
- package/src/lib/server/project-utils.ts +13 -0
- package/src/lib/server/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
- package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
- package/src/lib/server/provider-health.ts +18 -0
- package/src/lib/server/query-expansion.ts +4 -1
- package/src/lib/server/runtime/alert-dispatch.ts +8 -7
- package/src/lib/server/runtime/daemon-policy.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
- package/src/lib/server/runtime/daemon-state/health.ts +6 -0
- package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
- package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
- package/src/lib/server/runtime/daemon-state.test.ts +48 -0
- package/src/lib/server/runtime/daemon-state.ts +3 -1331
- package/src/lib/server/runtime/estop-repository.ts +4 -0
- package/src/lib/server/runtime/estop.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +78 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +6 -3
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/perf.ts +4 -1
- package/src/lib/server/runtime/process-manager.ts +7 -4
- package/src/lib/server/runtime/queue/claims.ts +4 -0
- package/src/lib/server/runtime/queue/core.ts +2079 -0
- package/src/lib/server/runtime/queue/execution.ts +7 -0
- package/src/lib/server/runtime/queue/followups.ts +4 -0
- package/src/lib/server/runtime/queue/queries.ts +12 -0
- package/src/lib/server/runtime/queue/recovery.ts +7 -0
- package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
- package/src/lib/server/runtime/queue-repository.ts +17 -0
- package/src/lib/server/runtime/queue.ts +5 -2058
- package/src/lib/server/runtime/run-ledger.ts +6 -5
- package/src/lib/server/runtime/run-repository.ts +73 -0
- package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
- package/src/lib/server/runtime/runtime-settings.ts +1 -1
- package/src/lib/server/runtime/runtime-state.ts +99 -0
- package/src/lib/server/runtime/scheduler.ts +13 -8
- package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
- package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
- package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
- package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
- package/src/lib/server/runtime/session-run-manager.ts +72 -1374
- package/src/lib/server/runtime/watch-job-repository.ts +35 -0
- package/src/lib/server/runtime/watch-jobs.ts +3 -1
- package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
- package/src/lib/server/sandbox/novnc-auth.ts +10 -0
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/session-tools/context.ts +14 -0
- package/src/lib/server/session-tools/discovery.ts +9 -6
- package/src/lib/server/session-tools/index.ts +3 -1
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/subagent.ts +23 -2
- package/src/lib/server/session-tools/wallet.ts +4 -1
- package/src/lib/server/sessions/session-repository.ts +85 -0
- package/src/lib/server/settings/settings-repository.ts +25 -0
- package/src/lib/server/skills/clawhub-client.ts +4 -1
- package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
- package/src/lib/server/skills/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-eligibility.ts +6 -0
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/solana.ts +6 -0
- package/src/lib/server/storage-auth.ts +5 -5
- package/src/lib/server/storage-normalization.ts +4 -0
- package/src/lib/server/storage.ts +32 -32
- package/src/lib/server/tasks/task-followups.ts +4 -1
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/tool-loop-detection.ts +8 -3
- package/src/lib/server/tool-planning.ts +226 -0
- package/src/lib/server/tool-retry.ts +4 -3
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/server/ws-hub.ts +5 -2
- package/src/lib/strip-internal-metadata.test.ts +78 -37
- package/src/lib/strip-internal-metadata.ts +20 -6
- package/src/stores/use-approval-store.ts +7 -1
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +26 -6
- package/src/types/index.ts +6 -0
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -43,16 +43,17 @@ describe('chat-streaming-state', () => {
|
|
|
43
43
|
localStreaming: true,
|
|
44
44
|
hasLiveArtifacts: true,
|
|
45
45
|
assistantRenderId: 'render-1',
|
|
46
|
+
showLiveRow: true,
|
|
47
|
+
syntheticAssistant: messages[1],
|
|
46
48
|
} as StreamingAwareMessageListOptions),
|
|
47
49
|
[
|
|
48
50
|
{ role: 'user', text: 'hello', time: 1 },
|
|
49
51
|
{
|
|
50
52
|
role: 'assistant',
|
|
51
|
-
text: '
|
|
52
|
-
time:
|
|
53
|
+
text: 'partial',
|
|
54
|
+
time: 2,
|
|
53
55
|
kind: 'chat',
|
|
54
56
|
streaming: true,
|
|
55
|
-
thinking: 'working...',
|
|
56
57
|
clientRenderId: 'render-1',
|
|
57
58
|
},
|
|
58
59
|
],
|
|
@@ -69,6 +70,7 @@ describe('chat-streaming-state', () => {
|
|
|
69
70
|
localStreaming: true,
|
|
70
71
|
hasLiveArtifacts: true,
|
|
71
72
|
assistantRenderId: 'render-2',
|
|
73
|
+
showLiveRow: true,
|
|
72
74
|
} as StreamingAwareMessageListOptions),
|
|
73
75
|
[
|
|
74
76
|
{ role: 'user', text: 'hello', time: 1 },
|
|
@@ -84,6 +86,43 @@ describe('chat-streaming-state', () => {
|
|
|
84
86
|
)
|
|
85
87
|
})
|
|
86
88
|
|
|
89
|
+
it('can show a synthetic live row for server-driven runs using the latest persisted streaming artifact', () => {
|
|
90
|
+
const messages: Message[] = [
|
|
91
|
+
{ role: 'user', text: 'queued follow-up', time: 1 },
|
|
92
|
+
{
|
|
93
|
+
role: 'assistant',
|
|
94
|
+
text: 'Drafting the handoff now.',
|
|
95
|
+
time: 2,
|
|
96
|
+
streaming: true,
|
|
97
|
+
thinking: 'Collecting the completion details',
|
|
98
|
+
toolEvents: [{ name: 'send_file', input: '{}', output: '/api/uploads/deck.pdf' }],
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
buildStreamingAwareMessageList(messages, {
|
|
104
|
+
localStreaming: true,
|
|
105
|
+
hasLiveArtifacts: false,
|
|
106
|
+
assistantRenderId: 'render-server',
|
|
107
|
+
showLiveRow: true,
|
|
108
|
+
syntheticAssistant: messages[1],
|
|
109
|
+
}),
|
|
110
|
+
[
|
|
111
|
+
{ role: 'user', text: 'queued follow-up', time: 1 },
|
|
112
|
+
{
|
|
113
|
+
role: 'assistant',
|
|
114
|
+
text: 'Drafting the handoff now.',
|
|
115
|
+
time: 2,
|
|
116
|
+
kind: 'chat',
|
|
117
|
+
streaming: true,
|
|
118
|
+
thinking: 'Collecting the completion details',
|
|
119
|
+
toolEvents: [{ name: 'send_file', input: '{}', output: '/api/uploads/deck.pdf' }],
|
|
120
|
+
clientRenderId: 'render-server',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
87
126
|
it('replaces trailing streaming assistant messages with the completed assistant message', () => {
|
|
88
127
|
const messages: Message[] = [
|
|
89
128
|
{ role: 'user', text: 'hello', time: 1 },
|
|
@@ -10,6 +10,8 @@ export interface StreamingAwareMessageListOptions {
|
|
|
10
10
|
localStreaming: boolean
|
|
11
11
|
hasLiveArtifacts: boolean
|
|
12
12
|
assistantRenderId?: string | null
|
|
13
|
+
showLiveRow?: boolean
|
|
14
|
+
syntheticAssistant?: Partial<Message> | null
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function isStreamingAssistantMessage(
|
|
@@ -27,13 +29,13 @@ function isStreamingAssistantMessage(
|
|
|
27
29
|
|
|
28
30
|
export function shouldHidePersistedStreamingAssistantMessage(
|
|
29
31
|
message: Message,
|
|
30
|
-
opts: { localStreaming: boolean; hasLiveArtifacts: boolean },
|
|
32
|
+
opts: { localStreaming: boolean; hasLiveArtifacts: boolean; showLiveRow?: boolean },
|
|
31
33
|
): boolean {
|
|
34
|
+
const showLiveRow = opts.showLiveRow ?? (opts.localStreaming && opts.hasLiveArtifacts)
|
|
32
35
|
return (
|
|
33
|
-
|
|
36
|
+
showLiveRow
|
|
34
37
|
&& message.role === 'assistant'
|
|
35
38
|
&& message.streaming === true
|
|
36
|
-
&& opts.hasLiveArtifacts
|
|
37
39
|
)
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -41,22 +43,32 @@ export function buildStreamingAwareMessageList(
|
|
|
41
43
|
messages: Message[],
|
|
42
44
|
opts: StreamingAwareMessageListOptions,
|
|
43
45
|
): Message[] {
|
|
44
|
-
const
|
|
46
|
+
const showLiveRow = opts.showLiveRow ?? (opts.localStreaming && opts.hasLiveArtifacts)
|
|
47
|
+
const nextMessages = showLiveRow
|
|
45
48
|
? messages.filter((message) => !shouldHidePersistedStreamingAssistantMessage(message, opts))
|
|
46
49
|
: messages
|
|
47
50
|
|
|
48
|
-
if (!
|
|
51
|
+
if (!showLiveRow || !opts.assistantRenderId) {
|
|
49
52
|
return nextMessages
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const syntheticAssistantMessage: Message = {
|
|
53
56
|
role: 'assistant',
|
|
54
|
-
text: '',
|
|
55
|
-
time: 0,
|
|
56
|
-
kind: 'chat',
|
|
57
|
+
text: typeof opts.syntheticAssistant?.text === 'string' ? opts.syntheticAssistant.text : '',
|
|
58
|
+
time: typeof opts.syntheticAssistant?.time === 'number' ? opts.syntheticAssistant.time : 0,
|
|
59
|
+
kind: opts.syntheticAssistant?.kind || 'chat',
|
|
57
60
|
streaming: true,
|
|
58
61
|
clientRenderId: opts.assistantRenderId,
|
|
59
62
|
}
|
|
63
|
+
if (typeof opts.syntheticAssistant?.thinking === 'string') {
|
|
64
|
+
syntheticAssistantMessage.thinking = opts.syntheticAssistant.thinking
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(opts.syntheticAssistant?.toolEvents)) {
|
|
67
|
+
syntheticAssistantMessage.toolEvents = opts.syntheticAssistant.toolEvents
|
|
68
|
+
}
|
|
69
|
+
if (opts.syntheticAssistant?.source) {
|
|
70
|
+
syntheticAssistantMessage.source = opts.syntheticAssistant.source
|
|
71
|
+
}
|
|
60
72
|
|
|
61
73
|
return [
|
|
62
74
|
...nextMessages,
|
package/src/lib/chat/chat.ts
CHANGED
|
@@ -53,10 +53,26 @@ export async function streamChat(
|
|
|
53
53
|
const reader = res.body.getReader()
|
|
54
54
|
const decoder = new TextDecoder()
|
|
55
55
|
let buf = ''
|
|
56
|
+
const STREAM_IDLE_TIMEOUT_MS = 300_000
|
|
56
57
|
|
|
57
58
|
while (true) {
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
60
|
+
let timedOut = false
|
|
61
|
+
const idleAbort = new Promise<{ done: true; value: undefined }>((resolve) => {
|
|
62
|
+
timeoutId = setTimeout(() => {
|
|
63
|
+
timedOut = true
|
|
64
|
+
resolve({ done: true, value: undefined })
|
|
65
|
+
}, STREAM_IDLE_TIMEOUT_MS)
|
|
66
|
+
})
|
|
67
|
+
const { done, value } = await Promise.race([reader.read(), idleAbort])
|
|
68
|
+
clearTimeout(timeoutId)
|
|
69
|
+
if (done) {
|
|
70
|
+
if (timedOut) {
|
|
71
|
+
onEvent?.({ t: 'err', text: 'Stream timed out (no data for 5 minutes)' })
|
|
72
|
+
reader.cancel().catch(() => {})
|
|
73
|
+
}
|
|
74
|
+
break
|
|
75
|
+
}
|
|
60
76
|
buf += decoder.decode(value, { stream: true })
|
|
61
77
|
const lines = buf.split('\n')
|
|
62
78
|
buf = lines.pop() || ''
|
|
@@ -27,7 +27,7 @@ describe('queued-message-queue', () => {
|
|
|
27
27
|
it('replaces queued items only for the requested session', () => {
|
|
28
28
|
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
29
29
|
{ runId: 'q4', sessionId: 'session-a', text: 'replacement', queuedAt: 4, position: 1 },
|
|
30
|
-
])
|
|
30
|
+
], { activeRunId: null })
|
|
31
31
|
assert.deepEqual(
|
|
32
32
|
listQueuedMessagesForSession(replaced, 'session-a').map((item) => item.runId),
|
|
33
33
|
['q4'],
|
|
@@ -38,6 +38,28 @@ describe('queued-message-queue', () => {
|
|
|
38
38
|
)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
it('keeps only the newly active run as a sending placeholder when it disappears from the queue snapshot', () => {
|
|
42
|
+
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
43
|
+
{ runId: 'q3', sessionId: 'session-a', text: 'second a', queuedAt: 3, position: 1 },
|
|
44
|
+
], { activeRunId: 'q1' })
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(
|
|
47
|
+
listQueuedMessagesForSession(replaced, 'session-a').map((item) => [item.runId, item.sending === true]),
|
|
48
|
+
[['q1', true], ['q3', false]],
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('drops missing stale queue rows that are not the active run', () => {
|
|
53
|
+
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
54
|
+
{ runId: 'q3', sessionId: 'session-a', text: 'second a', queuedAt: 3, position: 1 },
|
|
55
|
+
], { activeRunId: 'run-other' })
|
|
56
|
+
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
listQueuedMessagesForSession(replaced, 'session-a').map((item) => item.runId),
|
|
59
|
+
['q3'],
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
41
63
|
it('removes queued items by stable id', () => {
|
|
42
64
|
assert.deepEqual(removeQueuedMessageById(queue, 'q2').map((item) => item.runId), ['q1', 'q3'])
|
|
43
65
|
})
|
|
@@ -41,18 +41,27 @@ export function snapshotToQueuedMessages(snapshot: SessionQueueSnapshot): Queued
|
|
|
41
41
|
return snapshot.items.map((item) => ({ ...item }))
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
interface ReplaceQueuedMessagesOptions {
|
|
45
|
+
activeRunId?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
export function replaceQueuedMessagesForSession(
|
|
45
49
|
queue: QueuedSessionMessage[],
|
|
46
50
|
sessionId: string,
|
|
47
51
|
nextItems: QueuedSessionMessage[],
|
|
52
|
+
options: ReplaceQueuedMessagesOptions = {},
|
|
48
53
|
): QueuedSessionMessage[] {
|
|
49
54
|
const otherSessions = queue.filter((item) => item.sessionId !== sessionId)
|
|
50
55
|
const previousForSession = queue.filter((item) => item.sessionId === sessionId && !item.sending)
|
|
51
56
|
// Detect consumed messages: items in local state but not in server snapshot.
|
|
52
|
-
// Keep
|
|
57
|
+
// Keep only the run that actually became active visible as "sending" so it
|
|
58
|
+
// doesn't vanish from the UI before the transcript refresh catches up.
|
|
53
59
|
const nextRunIds = new Set(nextItems.map((item) => item.runId))
|
|
60
|
+
const activeRunId = typeof options.activeRunId === 'string' && options.activeRunId.trim()
|
|
61
|
+
? options.activeRunId
|
|
62
|
+
: null
|
|
54
63
|
const consumed = previousForSession
|
|
55
|
-
.filter((item) => !item.optimistic && !nextRunIds.has(item.runId))
|
|
64
|
+
.filter((item) => !item.optimistic && !nextRunIds.has(item.runId) && activeRunId === item.runId)
|
|
56
65
|
.map((item) => ({ ...item, sending: true }))
|
|
57
66
|
return [
|
|
58
67
|
...otherSessions,
|
|
@@ -2,8 +2,11 @@ import fs from 'fs'
|
|
|
2
2
|
import https from 'https'
|
|
3
3
|
import type { StreamChatOptions } from './index'
|
|
4
4
|
import { PROVIDER_DEFAULTS, IMAGE_EXTS, TEXT_EXTS, ANTHROPIC_MAX_TOKENS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
|
|
5
|
+
import { log } from '@/lib/server/logger'
|
|
5
6
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
6
7
|
|
|
8
|
+
const TAG = 'provider-anthropic'
|
|
9
|
+
|
|
7
10
|
async function fileToContentBlocks(filePath: string): Promise<Array<Record<string, unknown>>> {
|
|
8
11
|
if (!filePath || !fs.existsSync(filePath)) return []
|
|
9
12
|
if (IMAGE_EXTS.test(filePath)) {
|
|
@@ -71,7 +74,7 @@ export function streamAnthropicChat({ session, message, imagePath, apiKey, syste
|
|
|
71
74
|
apiRes.on('data', (c: Buffer) => errBody += c)
|
|
72
75
|
apiRes.on('end', () => {
|
|
73
76
|
const msg = `Anthropic error ${apiRes.statusCode}: ${errBody.slice(0, 200)}`
|
|
74
|
-
|
|
77
|
+
log.error(TAG, `[${session.id}] ${msg}`)
|
|
75
78
|
let errMsg = `Anthropic API error (${apiRes.statusCode})`
|
|
76
79
|
try {
|
|
77
80
|
const parsed = JSON.parse(errBody)
|
|
@@ -124,12 +127,12 @@ export function streamAnthropicChat({ session, message, imagePath, apiKey, syste
|
|
|
124
127
|
active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
|
|
125
128
|
|
|
126
129
|
apiReq.on('timeout', () => {
|
|
127
|
-
|
|
130
|
+
log.error(TAG, `[${session.id}] anthropic request timed out after 60s`)
|
|
128
131
|
apiReq.destroy(new Error('Request timed out after 60s'))
|
|
129
132
|
})
|
|
130
133
|
|
|
131
134
|
apiReq.on('error', (e) => {
|
|
132
|
-
|
|
135
|
+
log.error(TAG, `[${session.id}] anthropic request error:`, e.message)
|
|
133
136
|
writeSSE(write, 'err', e.message)
|
|
134
137
|
active.delete(session.id)
|
|
135
138
|
reject(e)
|
|
@@ -6,7 +6,9 @@ import type { StreamChatOptions } from './index'
|
|
|
6
6
|
import { log } from '../server/logger'
|
|
7
7
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
8
8
|
import { getEnabledToolIds } from '@/lib/capability-selection'
|
|
9
|
-
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
|
|
9
|
+
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
|
|
10
|
+
|
|
11
|
+
const TAG = 'provider-claude-cli'
|
|
10
12
|
|
|
11
13
|
export function streamClaudeCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
|
|
12
14
|
const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
|
|
@@ -152,8 +154,12 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
|
|
|
152
154
|
const text = chunk.toString()
|
|
153
155
|
stderrText += text
|
|
154
156
|
if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
if (isStderrNoise(text)) {
|
|
158
|
+
log.debug('claude-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
|
|
159
|
+
} else {
|
|
160
|
+
log.warn('claude-cli', `stderr [${session.id}]`, text.slice(0, 500))
|
|
161
|
+
log.error(TAG, `[${session.id}] stderr:`, text.slice(0, 200))
|
|
162
|
+
}
|
|
157
163
|
})
|
|
158
164
|
|
|
159
165
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { isStderrNoise, buildCliEnv, isCliProvider, CLI_PROVIDER_CAPABILITIES } from './cli-utils'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// isStderrNoise
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
describe('isStderrNoise', () => {
|
|
11
|
+
it('returns true for MallocStackLogging lines', () => {
|
|
12
|
+
assert.equal(isStderrNoise('MallocStackLogging: could not tag MSL'), true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns true for blank/whitespace lines', () => {
|
|
16
|
+
assert.equal(isStderrNoise(''), true)
|
|
17
|
+
assert.equal(isStderrNoise(' '), true)
|
|
18
|
+
assert.equal(isStderrNoise('\t'), true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns false for real error text', () => {
|
|
22
|
+
assert.equal(isStderrNoise('Error: connection refused'), false)
|
|
23
|
+
assert.equal(isStderrNoise('FATAL: segfault'), false)
|
|
24
|
+
assert.equal(isStderrNoise('Permission denied'), false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// buildCliEnv
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
describe('buildCliEnv', () => {
|
|
33
|
+
it('strips SWARMCLAW_ prefixed vars from env', () => {
|
|
34
|
+
const orig = process.env.SWARMCLAW_TEST_VAR
|
|
35
|
+
process.env.SWARMCLAW_TEST_VAR = 'should_be_stripped'
|
|
36
|
+
try {
|
|
37
|
+
const env = buildCliEnv()
|
|
38
|
+
assert.equal(env.SWARMCLAW_TEST_VAR, undefined)
|
|
39
|
+
} finally {
|
|
40
|
+
if (orig === undefined) delete process.env.SWARMCLAW_TEST_VAR
|
|
41
|
+
else process.env.SWARMCLAW_TEST_VAR = orig
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('deletes MallocStackLogging', () => {
|
|
46
|
+
const orig = process.env.MallocStackLogging
|
|
47
|
+
process.env.MallocStackLogging = '1'
|
|
48
|
+
try {
|
|
49
|
+
const env = buildCliEnv()
|
|
50
|
+
assert.equal(env.MallocStackLogging, undefined)
|
|
51
|
+
} finally {
|
|
52
|
+
if (orig === undefined) delete process.env.MallocStackLogging
|
|
53
|
+
else process.env.MallocStackLogging = orig
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('injects provided key-value pairs', () => {
|
|
58
|
+
const env = buildCliEnv({ inject: { MY_CUSTOM_KEY: 'hello' } })
|
|
59
|
+
assert.equal(env.MY_CUSTOM_KEY, 'hello')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('preserves unrelated user env vars', () => {
|
|
63
|
+
const env = buildCliEnv()
|
|
64
|
+
assert.equal(env.PATH, process.env.PATH)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('supports custom stripPrefixes', () => {
|
|
68
|
+
const orig = process.env.CUSTOM_PREFIX_VAR
|
|
69
|
+
process.env.CUSTOM_PREFIX_VAR = 'should_be_stripped'
|
|
70
|
+
try {
|
|
71
|
+
const env = buildCliEnv({ stripPrefixes: ['CUSTOM_PREFIX_'] })
|
|
72
|
+
assert.equal(env.CUSTOM_PREFIX_VAR, undefined)
|
|
73
|
+
} finally {
|
|
74
|
+
if (orig === undefined) delete process.env.CUSTOM_PREFIX_VAR
|
|
75
|
+
else process.env.CUSTOM_PREFIX_VAR = orig
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('sets TERM=dumb and NO_COLOR=1', () => {
|
|
80
|
+
const env = buildCliEnv()
|
|
81
|
+
assert.equal(env.TERM, 'dumb')
|
|
82
|
+
assert.equal(env.NO_COLOR, '1')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// isCliProvider
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
describe('isCliProvider', () => {
|
|
91
|
+
it('returns true for known CLI providers', () => {
|
|
92
|
+
assert.equal(isCliProvider('claude-cli'), true)
|
|
93
|
+
assert.equal(isCliProvider('codex-cli'), true)
|
|
94
|
+
assert.equal(isCliProvider('opencode-cli'), true)
|
|
95
|
+
assert.equal(isCliProvider('gemini-cli'), true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns false for non-CLI providers', () => {
|
|
99
|
+
assert.equal(isCliProvider('openai'), false)
|
|
100
|
+
assert.equal(isCliProvider('anthropic'), false)
|
|
101
|
+
assert.equal(isCliProvider(''), false)
|
|
102
|
+
assert.equal(isCliProvider('google'), false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// CLI_PROVIDER_CAPABILITIES
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('CLI_PROVIDER_CAPABILITIES', () => {
|
|
111
|
+
it('has entries for all 4 CLI providers', () => {
|
|
112
|
+
assert.ok('claude-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
113
|
+
assert.ok('codex-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
114
|
+
assert.ok('opencode-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
115
|
+
assert.ok('gemini-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('each entry is a non-empty string', () => {
|
|
119
|
+
for (const [key, value] of Object.entries(CLI_PROVIDER_CAPABILITIES)) {
|
|
120
|
+
assert.equal(typeof value, 'string', `${key} should be a string`)
|
|
121
|
+
assert.ok(value.length > 0, `${key} should be non-empty`)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -106,6 +106,8 @@ export function buildCliEnv(opts?: {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
delete (env as Record<string, unknown>).MallocStackLogging
|
|
110
|
+
|
|
109
111
|
if (opts?.inject) {
|
|
110
112
|
for (const [key, value] of Object.entries(opts.inject)) {
|
|
111
113
|
env[key] = value
|
|
@@ -115,6 +117,19 @@ export function buildCliEnv(opts?: {
|
|
|
115
117
|
return env
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Stderr Noise Filter
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
const STDERR_NOISE_PATTERNS: RegExp[] = [
|
|
125
|
+
/MallocStackLogging/,
|
|
126
|
+
/^\s*$/,
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
export function isStderrNoise(text: string): boolean {
|
|
130
|
+
return STDERR_NOISE_PATTERNS.some((re) => re.test(text))
|
|
131
|
+
}
|
|
132
|
+
|
|
118
133
|
// ---------------------------------------------------------------------------
|
|
119
134
|
// Auth Probing
|
|
120
135
|
// ---------------------------------------------------------------------------
|
|
@@ -5,7 +5,9 @@ import { spawn } from 'child_process'
|
|
|
5
5
|
import type { StreamChatOptions } from './index'
|
|
6
6
|
import { log } from '../server/logger'
|
|
7
7
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
8
|
-
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles } from './cli-utils'
|
|
8
|
+
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles, isStderrNoise } from './cli-utils'
|
|
9
|
+
|
|
10
|
+
const TAG = 'provider-codex'
|
|
9
11
|
|
|
10
12
|
function codexModelRequiresReasoningDowngrade(model: string | null | undefined): boolean {
|
|
11
13
|
const value = String(model || '').trim().toLowerCase()
|
|
@@ -196,8 +198,12 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
|
|
|
196
198
|
const text = chunk.toString()
|
|
197
199
|
stderrText += text
|
|
198
200
|
if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
if (isStderrNoise(text)) {
|
|
202
|
+
log.debug('codex-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
|
|
203
|
+
} else {
|
|
204
|
+
log.warn('codex-cli', `stderr [${session.id}]`, text.slice(0, 500))
|
|
205
|
+
log.error(TAG, `[${session.id}] codex stderr:`, text.slice(0, 200))
|
|
206
|
+
}
|
|
201
207
|
})
|
|
202
208
|
|
|
203
209
|
return new Promise((resolve) => {
|
|
@@ -2,7 +2,7 @@ import { spawn } from 'child_process'
|
|
|
2
2
|
import type { StreamChatOptions } from './index'
|
|
3
3
|
import { log } from '../server/logger'
|
|
4
4
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
5
|
-
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
|
|
5
|
+
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Gemini CLI provider — spawns `gemini --prompt <message> --output-format stream-json --yolo`.
|
|
@@ -151,7 +151,11 @@ export function streamGeminiCliChat({ session, message, imagePath, systemPrompt,
|
|
|
151
151
|
const text = chunk.toString()
|
|
152
152
|
stderrText += text
|
|
153
153
|
if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
|
|
154
|
-
|
|
154
|
+
if (isStderrNoise(text)) {
|
|
155
|
+
log.debug('gemini-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
|
|
156
|
+
} else {
|
|
157
|
+
log.warn('gemini-cli', `stderr [${session.id}]`, text.slice(0, 500))
|
|
158
|
+
}
|
|
155
159
|
})
|
|
156
160
|
|
|
157
161
|
return new Promise((resolve) => {
|
|
@@ -8,8 +8,11 @@ import { streamAnthropicChat } from './anthropic'
|
|
|
8
8
|
import { streamOpenClawChat } from './openclaw'
|
|
9
9
|
import { errorMessage, sleep, jitteredBackoff } from '@/lib/shared-utils'
|
|
10
10
|
import { classifyProviderError } from './error-classification'
|
|
11
|
+
import { log } from '@/lib/server/logger'
|
|
11
12
|
import type { ProviderInfo, ProviderConfig as CustomProviderConfig, ProviderType } from '../../types'
|
|
12
13
|
|
|
14
|
+
const TAG = 'providers'
|
|
15
|
+
|
|
13
16
|
export interface ProviderHandler {
|
|
14
17
|
streamChat: (opts: StreamChatOptions) => Promise<string>
|
|
15
18
|
}
|
|
@@ -439,7 +442,7 @@ export async function streamChatWithFailover(
|
|
|
439
442
|
if (classified.reason === 'auth_permanent') throw err
|
|
440
443
|
|
|
441
444
|
if (i < credentialIds.length - 1) {
|
|
442
|
-
|
|
445
|
+
log.info(TAG, `Credential ${credId} failed (${classified.reason}: ${errMessage?.slice(0, 80)}), trying fallback...`)
|
|
443
446
|
opts.write(`data: ${JSON.stringify({
|
|
444
447
|
t: 'md',
|
|
445
448
|
text: JSON.stringify({ failover: { from: credId, reason: errMessage?.slice(0, 100) } }),
|
|
@@ -3,9 +3,12 @@ import http from 'http'
|
|
|
3
3
|
import https from 'https'
|
|
4
4
|
import type { StreamChatOptions } from './index'
|
|
5
5
|
import { IMAGE_EXTS, TEXT_EXTS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
|
|
6
|
+
import { log } from '@/lib/server/logger'
|
|
6
7
|
import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
|
|
7
8
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
8
9
|
|
|
10
|
+
const TAG = 'provider-ollama'
|
|
11
|
+
|
|
9
12
|
export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
10
13
|
return new Promise((resolve, reject) => {
|
|
11
14
|
const messages = buildMessages(session, message, imagePath, loadHistory)
|
|
@@ -69,7 +72,7 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
69
72
|
apiRes.on('data', (c: Buffer) => errBody += c)
|
|
70
73
|
apiRes.on('end', () => {
|
|
71
74
|
const msg = `Ollama error ${apiRes.statusCode}: ${errBody.slice(0, 200)}`
|
|
72
|
-
|
|
75
|
+
log.error(TAG, `[${session.id}] ${msg}`)
|
|
73
76
|
writeSSE(write, 'err', msg.slice(0, 120))
|
|
74
77
|
active.delete(session.id)
|
|
75
78
|
reject(new Error(msg))
|
|
@@ -114,7 +117,7 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
114
117
|
active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
|
|
115
118
|
|
|
116
119
|
apiReq.on('error', (e: NodeJS.ErrnoException) => {
|
|
117
|
-
|
|
120
|
+
log.error(TAG, `[${session.id}] ollama request error:`, e.message)
|
|
118
121
|
let errMsg = e.message
|
|
119
122
|
if (e.code === 'ECONNREFUSED') {
|
|
120
123
|
errMsg = `Cannot connect to Ollama at ${endpoint}. Is Ollama running?`
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import type { StreamChatOptions } from './index'
|
|
3
3
|
import { PROVIDER_DEFAULTS, IMAGE_EXTS, TEXT_EXTS, PDF_MAX_CHARS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
|
|
4
|
+
import { log } from '@/lib/server/logger'
|
|
4
5
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
5
6
|
|
|
7
|
+
const TAG = 'provider-openai'
|
|
8
|
+
|
|
6
9
|
async function fileToContentParts(filePath: string): Promise<Array<Record<string, unknown>>> {
|
|
7
10
|
if (!filePath || !fs.existsSync(filePath)) return []
|
|
8
11
|
const name = filePath.split('/').pop() || 'file'
|
|
@@ -108,7 +111,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
108
111
|
const resContentType = res.headers.get('content-type') || ''
|
|
109
112
|
if (resContentType.includes('text/html')) {
|
|
110
113
|
const msg = 'Received HTML instead of API response. The endpoint may be misconfigured or returning a landing page.'
|
|
111
|
-
|
|
114
|
+
log.error(TAG, `[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
|
|
112
115
|
writeSSE(write, 'err', msg)
|
|
113
116
|
active.delete(session.id)
|
|
114
117
|
reject(new Error(msg))
|
|
@@ -117,7 +120,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
117
120
|
|
|
118
121
|
if (!res.ok) {
|
|
119
122
|
const errBody = await res.text().catch(() => '')
|
|
120
|
-
|
|
123
|
+
log.error(TAG, `[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
|
|
121
124
|
let errMsg = `API error (${res.status})`
|
|
122
125
|
try {
|
|
123
126
|
const parsed = JSON.parse(errBody)
|
|
@@ -133,7 +136,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
133
136
|
|
|
134
137
|
if (!res.body) {
|
|
135
138
|
const msg = `No response body from ${baseUrl}`
|
|
136
|
-
|
|
139
|
+
log.error(TAG, `[${session.id}] ${msg}`)
|
|
137
140
|
active.delete(session.id)
|
|
138
141
|
reject(new Error(msg))
|
|
139
142
|
return
|
|
@@ -175,12 +178,12 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
if (!fullResponse) {
|
|
178
|
-
|
|
181
|
+
log.error(TAG, `[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
|
|
179
182
|
}
|
|
180
183
|
} catch (err: unknown) {
|
|
181
184
|
const errObj = err as { name?: string; message?: string }
|
|
182
185
|
if (errObj.name !== 'AbortError') {
|
|
183
|
-
|
|
186
|
+
log.error(TAG, `[${session.id}] openai request error:`, errObj.message)
|
|
184
187
|
writeSSE(write, 'err', `Connection failed: ${errObj.message}`)
|
|
185
188
|
}
|
|
186
189
|
active.delete(session.id)
|
|
@@ -2,7 +2,7 @@ import { spawn } from 'child_process'
|
|
|
2
2
|
import type { StreamChatOptions } from './index'
|
|
3
3
|
import { log } from '../server/logger'
|
|
4
4
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
5
|
-
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
|
|
5
|
+
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* OpenCode CLI provider — spawns `opencode run <message> --format json` for non-interactive usage.
|
|
@@ -120,7 +120,11 @@ export function streamOpenCodeCliChat({ session, message, imagePath, systemPromp
|
|
|
120
120
|
const text = chunk.toString()
|
|
121
121
|
stderrText += text
|
|
122
122
|
if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
|
|
123
|
-
|
|
123
|
+
if (isStderrNoise(text)) {
|
|
124
|
+
log.debug('opencode-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
|
|
125
|
+
} else {
|
|
126
|
+
log.warn('opencode-cli', `stderr [${session.id}]`, text.slice(0, 500))
|
|
127
|
+
}
|
|
124
128
|
})
|
|
125
129
|
|
|
126
130
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadActivity as loadStoredActivity, logActivity as writeActivityLog } from '@/lib/server/storage'
|
|
2
|
+
import { perf } from '@/lib/server/runtime/perf'
|
|
3
|
+
|
|
4
|
+
export function loadActivity() {
|
|
5
|
+
return perf.measureSync('repository', 'activity.list', () => loadStoredActivity())
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function logActivity(entry: {
|
|
9
|
+
entityType: string
|
|
10
|
+
entityId: string
|
|
11
|
+
action: string
|
|
12
|
+
actor: string
|
|
13
|
+
actorId?: string
|
|
14
|
+
summary: string
|
|
15
|
+
detail?: Record<string, unknown>
|
|
16
|
+
}) {
|
|
17
|
+
perf.measureSync('repository', 'activity.log', () => writeActivityLog(entry), {
|
|
18
|
+
entityType: entry.entityType,
|
|
19
|
+
action: entry.action,
|
|
20
|
+
})
|
|
21
|
+
}
|