@swarmclawai/swarmclaw 1.9.37 → 1.9.39
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 +43 -1
- package/package.json +2 -2
- package/src/app/api/chats/[id]/context-status/route.ts +2 -0
- package/src/app/api/chats/context-status-route.test.ts +59 -0
- package/src/app/api/setup/check-provider/route.test.ts +12 -0
- package/src/app/api/setup/check-provider/route.ts +6 -0
- package/src/lib/providers/index.ts +23 -0
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +10 -1
- package/src/lib/server/connectors/outbox.ts +22 -2
- package/src/lib/server/context-manager.ts +4 -0
- package/src/lib/server/openrouter-model-context.test.ts +205 -0
- package/src/lib/server/openrouter-model-context.ts +169 -0
- package/src/lib/server/provider-health.ts +1 -0
- package/src/lib/server/runtime/queue/core.ts +160 -18
- package/src/lib/server/runtime/queue/orphan-recovery.test.ts +49 -0
- package/src/lib/server/runtime/queue/orphan-recovery.ts +32 -0
- package/src/lib/server/runtime/scheduled-run-preflight.test.ts +73 -0
- package/src/lib/server/runtime/scheduled-run-preflight.ts +83 -0
- package/src/lib/server/schedules/schedule-lifecycle.test.ts +44 -0
- package/src/lib/server/schedules/schedule-lifecycle.ts +27 -0
- package/src/lib/server/storage-normalization.ts +13 -0
- package/src/lib/server/tasks/task-followups.test.ts +124 -41
- package/src/lib/server/tasks/task-followups.ts +28 -3
- package/src/lib/server/tasks/task-lifecycle.test.ts +25 -0
- package/src/lib/server/tasks/task-lifecycle.ts +6 -0
- package/src/lib/server/tasks/task-result.test.ts +25 -1
- package/src/lib/server/tasks/task-result.ts +22 -0
- package/src/lib/server/workspace-paths.test.ts +72 -0
- package/src/lib/server/workspace-paths.ts +60 -0
- package/src/lib/setup-defaults.test.ts +10 -1
- package/src/lib/setup-defaults.ts +20 -0
- package/src/types/provider.ts +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ Available for macOS (Apple Silicon & Intel), Windows, and Linux (AppImage + .deb
|
|
|
84
84
|
The release workflow supports Developer ID signing and notarization when Apple
|
|
85
85
|
credentials are configured. If a macOS build is still ad-hoc signed, first
|
|
86
86
|
launch may need one manual approval:
|
|
87
|
-
- **macOS:**
|
|
87
|
+
- **macOS:** signed/notarized releases publish both `.dmg` and `.zip`; unsigned fallback releases publish `.zip` only to avoid the damaged unsigned DMG path. Right-click the app in Finder → **Open** → **Open** to bypass Gatekeeper. If macOS instead reports *"SwarmClaw is damaged and can't be opened"* (common when a downloaded app was quarantined by Safari), strip the quarantine attribute and relaunch:
|
|
88
88
|
```bash
|
|
89
89
|
xattr -dr com.apple.quarantine /Applications/SwarmClaw.app
|
|
90
90
|
```
|
|
@@ -151,6 +151,27 @@ openclaw skills install swarmclaw
|
|
|
151
151
|
|
|
152
152
|
[Browse on ClawHub](https://clawhub.ai/waydelyle/swarmclaw)
|
|
153
153
|
|
|
154
|
+
## v1.9.39 Highlights
|
|
155
|
+
|
|
156
|
+
Scheduled-run reliability release: stale workspace rebinding, fail-fast credential checks, and durable delivery evidence for schedule-driven sends.
|
|
157
|
+
|
|
158
|
+
- **Legacy workspace cwd migration.** Scheduled reruns and reused schedule sessions pinned to a pre-migration workspace root (e.g. `~/.swarmclaw/workspace`) are now rebound to the current `WORKSPACE_DIR` automatically; intentional custom working directories are never touched.
|
|
159
|
+
- **Orphan recovery dedup.** Startup queue recovery now recovers each orphaned task once instead of re-logging it every tick, and dead-letters tasks that repeatedly return to the queue without starting.
|
|
160
|
+
- **Schedule delivery status.** Every scheduled run now writes `lastDeliveryStatus`/`lastDeliveryError` back to its schedule, so failures are visible without digging through task records.
|
|
161
|
+
- **Credential preflight for schedules.** Scheduled runs on API-key providers fail fast with an actionable error when no credential resolves, instead of dying on a 401 deep in execution.
|
|
162
|
+
- **Empty-run classification.** Runs that produce no text, no tool calls, and no error now fail with a clear provider-configuration message instead of a generic validation failure.
|
|
163
|
+
- **Durable connector delivery evidence.** Task follow-up sends route through the connector outbox with task/schedule linkage, retries with backoff, and per-run dedupe, so triage can prove whether a scheduled send succeeded, failed, or was never attempted.
|
|
164
|
+
- **Regression coverage.** Added tests for workspace path normalization, orphan recovery, schedule outcome writes, credential preflight, empty-run classification, and outbox-backed follow-ups.
|
|
165
|
+
|
|
166
|
+
## v1.9.38 Highlights
|
|
167
|
+
|
|
168
|
+
PR integration release for provider catalog coverage, OpenRouter context meters, and safer unsigned macOS desktop artifacts.
|
|
169
|
+
|
|
170
|
+
- **TokenMix provider.** Added TokenMix as a built-in OpenAI-compatible provider with setup metadata, starter-agent defaults, and provider health checks.
|
|
171
|
+
- **OpenRouter context meters.** Chat context status now uses cached OpenRouter model metadata when available so routed model context windows display accurately.
|
|
172
|
+
- **macOS unsigned artifact fallback.** Desktop releases publish zip-only macOS artifacts when signing/notarization inputs are missing, avoiding the unsigned DMG damaged-app path.
|
|
173
|
+
- **Regression coverage.** Added targeted tests for TokenMix setup, OpenRouter context metadata caching, and macOS target selection.
|
|
174
|
+
|
|
154
175
|
## v1.9.37 Highlights
|
|
155
176
|
|
|
156
177
|
Theme and memory-pressure release for lighter UI preferences and leaner chat history storage.
|
|
@@ -479,6 +500,27 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
479
500
|
|
|
480
501
|
## Releases
|
|
481
502
|
|
|
503
|
+
### v1.9.39 Highlights
|
|
504
|
+
|
|
505
|
+
Scheduled-run reliability release: stale workspace rebinding, fail-fast credential checks, and durable delivery evidence for schedule-driven sends.
|
|
506
|
+
|
|
507
|
+
- **Legacy workspace cwd migration.** Scheduled reruns and reused schedule sessions pinned to a pre-migration workspace root (e.g. `~/.swarmclaw/workspace`) are now rebound to the current `WORKSPACE_DIR` automatically; intentional custom working directories are never touched.
|
|
508
|
+
- **Orphan recovery dedup.** Startup queue recovery now recovers each orphaned task once instead of re-logging it every tick, and dead-letters tasks that repeatedly return to the queue without starting.
|
|
509
|
+
- **Schedule delivery status.** Every scheduled run now writes `lastDeliveryStatus`/`lastDeliveryError` back to its schedule, so failures are visible without digging through task records.
|
|
510
|
+
- **Credential preflight for schedules.** Scheduled runs on API-key providers fail fast with an actionable error when no credential resolves, instead of dying on a 401 deep in execution.
|
|
511
|
+
- **Empty-run classification.** Runs that produce no text, no tool calls, and no error now fail with a clear provider-configuration message instead of a generic validation failure.
|
|
512
|
+
- **Durable connector delivery evidence.** Task follow-up sends route through the connector outbox with task/schedule linkage, retries with backoff, and per-run dedupe, so triage can prove whether a scheduled send succeeded, failed, or was never attempted.
|
|
513
|
+
- **Regression coverage.** Added tests for workspace path normalization, orphan recovery, schedule outcome writes, credential preflight, empty-run classification, and outbox-backed follow-ups.
|
|
514
|
+
|
|
515
|
+
### v1.9.38 Highlights
|
|
516
|
+
|
|
517
|
+
PR integration release for provider catalog coverage, OpenRouter context meters, and safer unsigned macOS desktop artifacts.
|
|
518
|
+
|
|
519
|
+
- **TokenMix provider.** Added TokenMix as a built-in OpenAI-compatible provider with setup metadata, starter-agent defaults, and provider health checks.
|
|
520
|
+
- **OpenRouter context meters.** Chat context status now uses cached OpenRouter model metadata when available so routed model context windows display accurately.
|
|
521
|
+
- **macOS unsigned artifact fallback.** Desktop releases publish zip-only macOS artifacts when signing/notarization inputs are missing, avoiding the unsigned DMG damaged-app path.
|
|
522
|
+
- **Regression coverage.** Added targeted tests for TokenMix setup, OpenRouter context metadata caching, and macOS target selection.
|
|
523
|
+
|
|
482
524
|
### v1.9.37 Highlights
|
|
483
525
|
|
|
484
526
|
Theme and memory-pressure release for lighter UI preferences and leaner chat history storage.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.39",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
89
89
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
90
90
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
91
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/autonomy/supervisor-settings.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/autonomy/supervisor-reflection.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/memory/dream-service.test.ts src/lib/server/memory/memory-consolidation.test.ts src/lib/server/messages/message-repository.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/compaction-generation-preference.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/connectors/slack.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/search/route.test.ts src/app/api/settings/settings-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/gateways/control-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
91
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/autonomy/supervisor-settings.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/autonomy/supervisor-reflection.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/memory/dream-service.test.ts src/lib/server/memory/memory-consolidation.test.ts src/lib/server/messages/message-repository.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/compaction-generation-preference.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/connectors/slack.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/tasks/task-lifecycle.test.ts src/lib/server/tasks/task-followups.test.ts src/lib/server/tasks/task-result.test.ts src/lib/server/schedules/schedule-lifecycle.test.ts src/lib/server/workspace-paths.test.ts src/lib/server/runtime/queue/orphan-recovery.test.ts src/lib/server/runtime/scheduled-run-preflight.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/search/route.test.ts src/app/api/settings/settings-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/gateways/control-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
92
92
|
"test:builder": "tsx --test src/features/protocols/builder/utils/builder-template-access.test.ts src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
93
93
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
94
94
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -3,6 +3,7 @@ import { getSession } from '@/lib/server/sessions/session-repository'
|
|
|
3
3
|
import { getMessages } from '@/lib/server/messages/message-repository'
|
|
4
4
|
import { getContextStatus } from '@/lib/server/context-manager'
|
|
5
5
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
6
|
+
import { ensureOpenRouterModelContextCache } from '@/lib/server/openrouter-model-context'
|
|
6
7
|
|
|
7
8
|
const SYSTEM_PROMPT_TOKEN_ESTIMATE = 2000
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
11
12
|
const session = getSession(id)
|
|
12
13
|
if (!session) return notFound()
|
|
13
14
|
const messages = getMessages(id)
|
|
15
|
+
await ensureOpenRouterModelContextCache(session.provider as string)
|
|
14
16
|
const status = getContextStatus(
|
|
15
17
|
messages,
|
|
16
18
|
SYSTEM_PROMPT_TOKEN_ESTIMATE,
|
|
@@ -66,3 +66,62 @@ test('GET /api/chats/[id]/context-status returns token usage summary', () => {
|
|
|
66
66
|
assert.ok(['ok', 'warning', 'critical'].includes(output.strategy))
|
|
67
67
|
assert.equal(output.missingStatus, 404)
|
|
68
68
|
})
|
|
69
|
+
|
|
70
|
+
test('GET /api/chats/[id]/context-status uses OpenRouter model metadata context window', () => {
|
|
71
|
+
const output = runWithTempDataDir<{
|
|
72
|
+
status: number
|
|
73
|
+
contextWindow: number
|
|
74
|
+
}>(`
|
|
75
|
+
const fs = await import('node:fs')
|
|
76
|
+
const path = await import('node:path')
|
|
77
|
+
const cachePath = path.join(process.env.DATA_DIR, 'openrouter-model-context.json')
|
|
78
|
+
fs.writeFileSync(cachePath, JSON.stringify({
|
|
79
|
+
loadedAt: Date.now(),
|
|
80
|
+
models: { 'minimax/minimax-m3': 524288 },
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
globalThis.fetch = async () => {
|
|
84
|
+
throw new Error('route should use seeded OpenRouter model metadata cache')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
88
|
+
const repoMod = await import('@/lib/server/messages/message-repository')
|
|
89
|
+
const routeMod = await import('./src/app/api/chats/[id]/context-status/route')
|
|
90
|
+
const storage = storageMod.default || storageMod
|
|
91
|
+
const repo = repoMod.default || repoMod
|
|
92
|
+
const route = routeMod.default || routeMod
|
|
93
|
+
|
|
94
|
+
const now = Date.now()
|
|
95
|
+
storage.saveSessions({
|
|
96
|
+
sess_ctx_openrouter: {
|
|
97
|
+
id: 'sess_ctx_openrouter',
|
|
98
|
+
name: 'OpenRouter context status test',
|
|
99
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
100
|
+
user: 'tester',
|
|
101
|
+
provider: 'openrouter',
|
|
102
|
+
model: 'minimax/minimax-m3',
|
|
103
|
+
claudeSessionId: null,
|
|
104
|
+
messages: [],
|
|
105
|
+
createdAt: now,
|
|
106
|
+
lastActiveAt: now,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
repo.appendMessage('sess_ctx_openrouter', { role: 'user', text: 'hello world', time: now })
|
|
111
|
+
|
|
112
|
+
const response = await route.GET(
|
|
113
|
+
new Request('http://local/api/chats/sess_ctx_openrouter/context-status'),
|
|
114
|
+
{ params: Promise.resolve({ id: 'sess_ctx_openrouter' }) },
|
|
115
|
+
)
|
|
116
|
+
const payload = await response.json()
|
|
117
|
+
|
|
118
|
+
console.log(JSON.stringify({
|
|
119
|
+
status: response.status,
|
|
120
|
+
contextWindow: payload.contextWindow,
|
|
121
|
+
}))
|
|
122
|
+
`, { prefix: 'swarmclaw-context-status-route-' })
|
|
123
|
+
|
|
124
|
+
assert.equal(output.status, 200)
|
|
125
|
+
assert.equal(output.contextWindow, 524_288)
|
|
126
|
+
assert.notEqual(output.contextWindow, 8_192)
|
|
127
|
+
})
|
|
@@ -161,3 +161,15 @@ test('POST returns provider diagnostics with normalized LM Studio targets and re
|
|
|
161
161
|
globalThis.fetch = originalFetch
|
|
162
162
|
}
|
|
163
163
|
})
|
|
164
|
+
|
|
165
|
+
test('POST rejects TokenMix setup checks without an API key', async () => {
|
|
166
|
+
const res = await POST(new Request('http://localhost/api/setup/check-provider', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
body: JSON.stringify({ provider: 'tokenmix' }),
|
|
169
|
+
}))
|
|
170
|
+
const payload = await res.json()
|
|
171
|
+
|
|
172
|
+
assert.equal(res.status, 200)
|
|
173
|
+
assert.equal(payload.ok, false)
|
|
174
|
+
assert.equal(payload.message, 'TokenMix API key is required.')
|
|
175
|
+
})
|
|
@@ -501,6 +501,12 @@ export async function POST(req: Request) {
|
|
|
501
501
|
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
|
|
502
502
|
return NextResponse.json(result)
|
|
503
503
|
}
|
|
504
|
+
case 'tokenmix': {
|
|
505
|
+
if (!apiKey) return NextResponse.json({ ok: false, message: 'TokenMix API key is required.' })
|
|
506
|
+
const info = OPENAI_COMPATIBLE_DEFAULTS.tokenmix
|
|
507
|
+
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
|
|
508
|
+
return NextResponse.json(result)
|
|
509
|
+
}
|
|
504
510
|
case 'anthropic': {
|
|
505
511
|
if (!apiKey) return NextResponse.json({ ok: false, message: 'Anthropic API key is required.' })
|
|
506
512
|
const result = await checkAnthropic(apiKey, endpoint, model)
|
|
@@ -128,6 +128,29 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
128
128
|
},
|
|
129
129
|
},
|
|
130
130
|
},
|
|
131
|
+
tokenmix: {
|
|
132
|
+
id: 'tokenmix',
|
|
133
|
+
name: 'TokenMix',
|
|
134
|
+
models: [
|
|
135
|
+
'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5',
|
|
136
|
+
'gpt-5.4', 'gpt-5.4-mini',
|
|
137
|
+
'gemini-2.5-pro', 'gemini-2.5-flash',
|
|
138
|
+
'deepseek-chat', 'deepseek-reasoner',
|
|
139
|
+
'qwen-max',
|
|
140
|
+
],
|
|
141
|
+
requiresApiKey: true,
|
|
142
|
+
requiresEndpoint: false,
|
|
143
|
+
defaultEndpoint: 'https://api.tokenmix.ai/v1',
|
|
144
|
+
handler: {
|
|
145
|
+
streamChat: (opts) => {
|
|
146
|
+
const patchedSession = {
|
|
147
|
+
...opts.session,
|
|
148
|
+
apiEndpoint: opts.session.apiEndpoint || 'https://api.tokenmix.ai/v1',
|
|
149
|
+
}
|
|
150
|
+
return streamOpenAiChat({ ...opts, session: patchedSession })
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
131
154
|
anthropic: {
|
|
132
155
|
id: 'anthropic',
|
|
133
156
|
name: 'Anthropic',
|
|
@@ -5,7 +5,7 @@ import path from 'node:path'
|
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { describe, it } from 'node:test'
|
|
7
7
|
|
|
8
|
-
import { assessAutonomyRun } from '@/lib/server/autonomy/supervisor-reflection'
|
|
8
|
+
import { assessAutonomyRun, classifyRuntimeFailure } from '@/lib/server/autonomy/supervisor-reflection'
|
|
9
9
|
import type { Session } from '@/types'
|
|
10
10
|
|
|
11
11
|
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
@@ -615,4 +615,13 @@ describe('supervisor-reflection', () => {
|
|
|
615
615
|
assert.doesNotMatch(String(incident?.details || ''), /<!doctype html>/i)
|
|
616
616
|
assert.match(String(incident?.details || ''), /singleton is not defined/i)
|
|
617
617
|
})
|
|
618
|
+
|
|
619
|
+
it('classifies the scheduled-run credential preflight error as provider auth', () => {
|
|
620
|
+
const failure = classifyRuntimeFailure({
|
|
621
|
+
source: 'task',
|
|
622
|
+
message: 'Provider authentication preflight failed: no API credential configured for provider "openai".',
|
|
623
|
+
})
|
|
624
|
+
assert.equal(failure.family, 'provider_auth')
|
|
625
|
+
assert.equal(failure.severity, 'high')
|
|
626
|
+
})
|
|
618
627
|
})
|
|
@@ -43,6 +43,10 @@ export interface ConnectorOutboxEntry extends Record<string, unknown> {
|
|
|
43
43
|
threadId?: string
|
|
44
44
|
ptt?: boolean
|
|
45
45
|
dedupeKey?: string | null
|
|
46
|
+
/** Originating task, when this entry delivers a task follow-up */
|
|
47
|
+
taskId?: string | null
|
|
48
|
+
/** Originating schedule, when the task was schedule-driven */
|
|
49
|
+
scheduleId?: string | null
|
|
46
50
|
lastError?: string | null
|
|
47
51
|
deliveredAt?: number | null
|
|
48
52
|
lastMessageId?: string | null
|
|
@@ -78,8 +82,8 @@ function normalizeEntry(value: unknown): ConnectorOutboxEntry | null {
|
|
|
78
82
|
if (!id || !channelId) return null
|
|
79
83
|
return {
|
|
80
84
|
id,
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
connectorId: typeof row.connectorId === 'string' ? row.connectorId : undefined,
|
|
86
|
+
platform: typeof row.platform === 'string' ? row.platform : undefined,
|
|
83
87
|
channelId,
|
|
84
88
|
text: typeof row.text === 'string' ? row.text : '',
|
|
85
89
|
sessionId: typeof row.sessionId === 'string' ? row.sessionId : null,
|
|
@@ -99,6 +103,8 @@ function normalizeEntry(value: unknown): ConnectorOutboxEntry | null {
|
|
|
99
103
|
attemptCount: typeof row.attemptCount === 'number' ? row.attemptCount : 0,
|
|
100
104
|
maxAttempts: typeof row.maxAttempts === 'number' ? row.maxAttempts : DEFAULT_MAX_ATTEMPTS,
|
|
101
105
|
dedupeKey: typeof row.dedupeKey === 'string' ? row.dedupeKey : null,
|
|
106
|
+
taskId: typeof row.taskId === 'string' ? row.taskId : null,
|
|
107
|
+
scheduleId: typeof row.scheduleId === 'string' ? row.scheduleId : null,
|
|
102
108
|
lastError: typeof row.lastError === 'string' ? row.lastError : null,
|
|
103
109
|
deliveredAt: typeof row.deliveredAt === 'number' ? row.deliveredAt : null,
|
|
104
110
|
lastMessageId: typeof row.lastMessageId === 'string' ? row.lastMessageId : null,
|
|
@@ -301,6 +307,20 @@ export function enqueueConnectorOutbox(
|
|
|
301
307
|
return { outboxId: entry.id, sendAt: entry.sendAt }
|
|
302
308
|
}
|
|
303
309
|
|
|
310
|
+
/**
|
|
311
|
+
* True when a delivery for this dedupe key already reached a successful
|
|
312
|
+
* terminal state. `findPendingConnectorOutboxByDedupe` only sees non-terminal
|
|
313
|
+
* entries, so it cannot prevent re-sends after a success.
|
|
314
|
+
*/
|
|
315
|
+
export function hasSentConnectorOutboxForDedupe(dedupeKey: string): boolean {
|
|
316
|
+
const normalizedKey = dedupeKey.trim()
|
|
317
|
+
if (!normalizedKey) return false
|
|
318
|
+
return listEntries().some((entry) =>
|
|
319
|
+
entry.dedupeKey === normalizedKey
|
|
320
|
+
&& (entry.status === 'sent' || entry.status === 'suppressed'),
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
304
324
|
export function findPendingConnectorOutboxByDedupe(dedupeKey: string, now = Date.now()): ConnectorOutboxEntry | null {
|
|
305
325
|
const normalizedKey = dedupeKey.trim()
|
|
306
326
|
if (!normalizedKey) return null
|
|
@@ -2,6 +2,7 @@ import type { Message, Session } from '@/types'
|
|
|
2
2
|
import { getMemoryDb } from '@/lib/server/memory/memory-db'
|
|
3
3
|
import { extractFactsFromMessages, ensureRunContext, pruneRunContext } from '@/lib/server/run-context'
|
|
4
4
|
import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
|
|
5
|
+
import { getCachedOpenRouterContextWindow } from '@/lib/server/openrouter-model-context'
|
|
5
6
|
|
|
6
7
|
import { repairTranscriptConsistency } from './transcript-repair'
|
|
7
8
|
|
|
@@ -97,6 +98,9 @@ const PROVIDER_DEFAULT_WINDOWS: Record<string, number> = {
|
|
|
97
98
|
|
|
98
99
|
/** Get context window size for a model, falling back to provider default */
|
|
99
100
|
export function getContextWindowSize(provider: string, model: string): number {
|
|
101
|
+
const openRouterContext = getCachedOpenRouterContextWindow(provider, model)
|
|
102
|
+
if (openRouterContext) return openRouterContext
|
|
103
|
+
|
|
100
104
|
return PROVIDER_CONTEXT_WINDOWS[model]
|
|
101
105
|
|| PROVIDER_DEFAULT_WINDOWS[provider]
|
|
102
106
|
|| 8_192
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
5
|
+
|
|
6
|
+
interface OpenRouterContextResult {
|
|
7
|
+
contextWindow: number | null
|
|
8
|
+
fetchCalls?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function runOpenRouterContextScript(script: string): OpenRouterContextResult {
|
|
12
|
+
return runWithTempDataDir<OpenRouterContextResult>(script, {
|
|
13
|
+
prefix: 'swarmclaw-openrouter-context-',
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('exact OpenRouter model ID returns cached context length', () => {
|
|
18
|
+
const output = runOpenRouterContextScript(`
|
|
19
|
+
const fs = await import('node:fs')
|
|
20
|
+
const path = await import('node:path')
|
|
21
|
+
const cachePath = path.join(process.env.DATA_DIR, 'openrouter-model-context.json')
|
|
22
|
+
fs.writeFileSync(cachePath, JSON.stringify({
|
|
23
|
+
loadedAt: Date.now(),
|
|
24
|
+
models: { 'minimax/minimax-m3': 524288 },
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
28
|
+
const mod = modImport.default || modImport
|
|
29
|
+
globalThis.fetch = async () => {
|
|
30
|
+
throw new Error('fetch should not run when cache is fresh')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await mod.ensureOpenRouterModelContextCache('openrouter')
|
|
34
|
+
console.log(JSON.stringify({
|
|
35
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openrouter', 'minimax/minimax-m3'),
|
|
36
|
+
}))
|
|
37
|
+
`)
|
|
38
|
+
|
|
39
|
+
assert.equal(output.contextWindow, 524_288)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('top_provider.context_length is preferred over context_length', () => {
|
|
43
|
+
const output = runOpenRouterContextScript(`
|
|
44
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
45
|
+
const mod = modImport.default || modImport
|
|
46
|
+
globalThis.fetch = async () => new Response(JSON.stringify({
|
|
47
|
+
data: [{
|
|
48
|
+
id: 'provider/model-a',
|
|
49
|
+
context_length: 8192,
|
|
50
|
+
top_provider: { context_length: 131072 },
|
|
51
|
+
}],
|
|
52
|
+
}), { status: 200 })
|
|
53
|
+
|
|
54
|
+
await mod.ensureOpenRouterModelContextCache('openrouter')
|
|
55
|
+
console.log(JSON.stringify({
|
|
56
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openrouter', 'provider/model-a'),
|
|
57
|
+
}))
|
|
58
|
+
`)
|
|
59
|
+
|
|
60
|
+
assert.equal(output.contextWindow, 131_072)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('unique suffix match works for unprefixed model IDs', () => {
|
|
64
|
+
const output = runOpenRouterContextScript(`
|
|
65
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
66
|
+
const mod = modImport.default || modImport
|
|
67
|
+
globalThis.fetch = async () => new Response(JSON.stringify({
|
|
68
|
+
data: [{ id: 'google/gemini-2.5-pro', context_length: 1048576 }],
|
|
69
|
+
}), { status: 200 })
|
|
70
|
+
|
|
71
|
+
await mod.ensureOpenRouterModelContextCache('openrouter')
|
|
72
|
+
console.log(JSON.stringify({
|
|
73
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openrouter', 'gemini-2.5-pro'),
|
|
74
|
+
}))
|
|
75
|
+
`)
|
|
76
|
+
|
|
77
|
+
assert.equal(output.contextWindow, 1_048_576)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('ambiguous suffix match returns null', () => {
|
|
81
|
+
const output = runOpenRouterContextScript(`
|
|
82
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
83
|
+
const mod = modImport.default || modImport
|
|
84
|
+
globalThis.fetch = async () => new Response(JSON.stringify({
|
|
85
|
+
data: [
|
|
86
|
+
{ id: 'provider-a/shared-model', context_length: 32000 },
|
|
87
|
+
{ id: 'provider-b/shared-model', context_length: 64000 },
|
|
88
|
+
],
|
|
89
|
+
}), { status: 200 })
|
|
90
|
+
|
|
91
|
+
await mod.ensureOpenRouterModelContextCache('openrouter')
|
|
92
|
+
console.log(JSON.stringify({
|
|
93
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openrouter', 'shared-model'),
|
|
94
|
+
}))
|
|
95
|
+
`)
|
|
96
|
+
|
|
97
|
+
assert.equal(output.contextWindow, null)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('non-OpenRouter provider returns null', () => {
|
|
101
|
+
const output = runOpenRouterContextScript(`
|
|
102
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
103
|
+
const mod = modImport.default || modImport
|
|
104
|
+
console.log(JSON.stringify({
|
|
105
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openai', 'minimax/minimax-m3'),
|
|
106
|
+
}))
|
|
107
|
+
`)
|
|
108
|
+
|
|
109
|
+
assert.equal(output.contextWindow, null)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('failed fetch does not throw', () => {
|
|
113
|
+
const output = runOpenRouterContextScript(`
|
|
114
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
115
|
+
const mod = modImport.default || modImport
|
|
116
|
+
let fetchCalls = 0
|
|
117
|
+
globalThis.fetch = async () => {
|
|
118
|
+
fetchCalls += 1
|
|
119
|
+
throw new Error('network down')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await mod.ensureOpenRouterModelContextCache('openrouter')
|
|
123
|
+
console.log(JSON.stringify({
|
|
124
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openrouter', 'minimax/minimax-m3'),
|
|
125
|
+
fetchCalls,
|
|
126
|
+
}))
|
|
127
|
+
`)
|
|
128
|
+
|
|
129
|
+
assert.equal(output.contextWindow, null)
|
|
130
|
+
assert.equal(output.fetchCalls, 1)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('timed out fetch does not throw', () => {
|
|
134
|
+
const output = runOpenRouterContextScript(`
|
|
135
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
136
|
+
const mod = modImport.default || modImport
|
|
137
|
+
let fetchCalls = 0
|
|
138
|
+
globalThis.fetch = async (_input, init) => {
|
|
139
|
+
fetchCalls += 1
|
|
140
|
+
await new Promise((_resolve, reject) => {
|
|
141
|
+
init.signal.addEventListener('abort', () => reject(init.signal.reason), { once: true })
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await mod.ensureOpenRouterModelContextCache('openrouter')
|
|
146
|
+
console.log(JSON.stringify({
|
|
147
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openrouter', 'minimax/minimax-m3'),
|
|
148
|
+
fetchCalls,
|
|
149
|
+
}))
|
|
150
|
+
`)
|
|
151
|
+
|
|
152
|
+
assert.equal(output.contextWindow, null)
|
|
153
|
+
assert.equal(output.fetchCalls, 1)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('stale cache is ignored', () => {
|
|
157
|
+
const output = runOpenRouterContextScript(`
|
|
158
|
+
const fs = await import('node:fs')
|
|
159
|
+
const path = await import('node:path')
|
|
160
|
+
const cachePath = path.join(process.env.DATA_DIR, 'openrouter-model-context.json')
|
|
161
|
+
fs.writeFileSync(cachePath, JSON.stringify({
|
|
162
|
+
loadedAt: Date.now() - (25 * 60 * 60 * 1000),
|
|
163
|
+
models: { 'minimax/minimax-m3': 524288 },
|
|
164
|
+
}))
|
|
165
|
+
|
|
166
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
167
|
+
const mod = modImport.default || modImport
|
|
168
|
+
let fetchCalls = 0
|
|
169
|
+
globalThis.fetch = async () => {
|
|
170
|
+
fetchCalls += 1
|
|
171
|
+
throw new Error('network down')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await mod.ensureOpenRouterModelContextCache('openrouter')
|
|
175
|
+
console.log(JSON.stringify({
|
|
176
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openrouter', 'minimax/minimax-m3'),
|
|
177
|
+
fetchCalls,
|
|
178
|
+
}))
|
|
179
|
+
`)
|
|
180
|
+
|
|
181
|
+
assert.equal(output.contextWindow, null)
|
|
182
|
+
assert.equal(output.fetchCalls, 1)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('cache write failure does not throw', () => {
|
|
186
|
+
const output = runOpenRouterContextScript(`
|
|
187
|
+
const fs = await import('node:fs')
|
|
188
|
+
const path = await import('node:path')
|
|
189
|
+
const cachePath = path.join(process.env.DATA_DIR, 'openrouter-model-context.json')
|
|
190
|
+
fs.mkdirSync(cachePath)
|
|
191
|
+
|
|
192
|
+
const modImport = await import('./src/lib/server/openrouter-model-context')
|
|
193
|
+
const mod = modImport.default || modImport
|
|
194
|
+
globalThis.fetch = async () => new Response(JSON.stringify({
|
|
195
|
+
data: [{ id: 'provider/model-a', context_length: 65536 }],
|
|
196
|
+
}), { status: 200 })
|
|
197
|
+
|
|
198
|
+
await mod.ensureOpenRouterModelContextCache('openrouter')
|
|
199
|
+
console.log(JSON.stringify({
|
|
200
|
+
contextWindow: mod.getCachedOpenRouterContextWindow('openrouter', 'provider/model-a'),
|
|
201
|
+
}))
|
|
202
|
+
`)
|
|
203
|
+
|
|
204
|
+
assert.equal(output.contextWindow, 65_536)
|
|
205
|
+
})
|