@swarmclawai/swarmclaw 0.7.1 → 0.7.3
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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process'
|
|
2
2
|
|
|
3
|
-
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'
|
|
3
|
+
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
4
4
|
|
|
5
5
|
interface ProviderHealthState {
|
|
6
6
|
failures: number
|
|
@@ -15,6 +15,7 @@ const states: Map<string, ProviderHealthState> =
|
|
|
15
15
|
(globalThis as any)[gk] ?? ((globalThis as any)[gk] = new Map<string, ProviderHealthState>())
|
|
16
16
|
|
|
17
17
|
const cliCheckCache = new Map<string, { at: number; ok: boolean }>()
|
|
18
|
+
const delegateReadyCache = new Map<string, { at: number; ok: boolean }>()
|
|
18
19
|
const CLI_CHECK_TTL_MS = 30_000
|
|
19
20
|
|
|
20
21
|
function commandExists(binary: string): boolean {
|
|
@@ -66,12 +67,43 @@ export function isProviderCoolingDown(providerId: string): boolean {
|
|
|
66
67
|
function delegateBinary(delegateTool: DelegateTool): string {
|
|
67
68
|
if (delegateTool === 'delegate_to_claude_code') return 'claude'
|
|
68
69
|
if (delegateTool === 'delegate_to_codex_cli') return 'codex'
|
|
70
|
+
if (delegateTool === 'delegate_to_gemini_cli') return 'gemini'
|
|
69
71
|
return 'opencode'
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
function delegateToolReady(delegateTool: DelegateTool): boolean {
|
|
75
|
+
const now = Date.now()
|
|
76
|
+
const cached = delegateReadyCache.get(delegateTool)
|
|
77
|
+
if (cached && now - cached.at < CLI_CHECK_TTL_MS) return cached.ok
|
|
78
|
+
|
|
79
|
+
const binary = delegateBinary(delegateTool)
|
|
80
|
+
let ok = commandExists(binary)
|
|
81
|
+
if (ok && delegateTool === 'delegate_to_claude_code') {
|
|
82
|
+
const probe = spawnSync(binary, ['auth', 'status'], { encoding: 'utf-8', timeout: 8000 })
|
|
83
|
+
if ((probe.status ?? 1) !== 0) {
|
|
84
|
+
let loggedIn = false
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(probe.stdout || '{}') as { loggedIn?: boolean }
|
|
87
|
+
loggedIn = parsed.loggedIn === true
|
|
88
|
+
} catch {
|
|
89
|
+
loggedIn = false
|
|
90
|
+
}
|
|
91
|
+
ok = loggedIn
|
|
92
|
+
}
|
|
93
|
+
} else if (ok && delegateTool === 'delegate_to_codex_cli') {
|
|
94
|
+
const probe = spawnSync(binary, ['login', 'status'], { encoding: 'utf-8', timeout: 8000 })
|
|
95
|
+
const probeText = `${probe.stdout || ''}\n${probe.stderr || ''}`.toLowerCase()
|
|
96
|
+
ok = (probe.status ?? 1) === 0 && probeText.includes('logged in')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
delegateReadyCache.set(delegateTool, { at: now, ok })
|
|
100
|
+
return ok
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
function delegateProviderId(delegateTool: DelegateTool): string {
|
|
73
104
|
if (delegateTool === 'delegate_to_claude_code') return 'claude-cli'
|
|
74
105
|
if (delegateTool === 'delegate_to_codex_cli') return 'codex-cli'
|
|
106
|
+
if (delegateTool === 'delegate_to_gemini_cli') return 'gemini-cli'
|
|
75
107
|
return 'opencode-cli'
|
|
76
108
|
}
|
|
77
109
|
|
|
@@ -83,9 +115,9 @@ export function rankDelegatesByHealth(order: DelegateTool[]): DelegateTool[] {
|
|
|
83
115
|
return true
|
|
84
116
|
})
|
|
85
117
|
return deduped.sort((a, b) => {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
118
|
+
const aReady = delegateToolReady(a)
|
|
119
|
+
const bReady = delegateToolReady(b)
|
|
120
|
+
if (aReady !== bReady) return aReady ? -1 : 1
|
|
89
121
|
|
|
90
122
|
const aCool = isProviderCoolingDown(delegateProviderId(a))
|
|
91
123
|
const bCool = isProviderCoolingDown(delegateProviderId(b))
|
|
@@ -202,14 +234,14 @@ export async function pingOpenClaw(
|
|
|
202
234
|
|
|
203
235
|
/**
|
|
204
236
|
* Ping a provider to check reachability. Returns `{ ok, message }`.
|
|
205
|
-
* Skips CLI-based providers (claude-cli, codex-cli, opencode-cli) — returns ok.
|
|
237
|
+
* Skips CLI-based providers (claude-cli, codex-cli, opencode-cli, gemini-cli) — returns ok.
|
|
206
238
|
*/
|
|
207
239
|
export async function pingProvider(
|
|
208
240
|
provider: string,
|
|
209
241
|
apiKey: string | undefined,
|
|
210
242
|
endpoint: string | undefined,
|
|
211
243
|
): Promise<{ ok: boolean; message: string }> {
|
|
212
|
-
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli']
|
|
244
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli']
|
|
213
245
|
if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
|
|
214
246
|
|
|
215
247
|
try {
|
package/src/lib/server/queue.ts
CHANGED
|
@@ -4,14 +4,13 @@ import path from 'node:path'
|
|
|
4
4
|
import { loadTasks, saveTasks, loadQueue, saveQueue, loadAgents, loadSchedules, saveSchedules, loadSessions, saveSessions, loadSettings, loadConnectors, UPLOAD_DIR } from './storage'
|
|
5
5
|
import { notify } from './ws-hub'
|
|
6
6
|
import { WORKSPACE_DIR } from './data-dir'
|
|
7
|
-
import { createOrchestratorSession
|
|
7
|
+
import { createOrchestratorSession } from './orchestrator'
|
|
8
8
|
import { formatValidationFailure, validateTaskCompletion } from './task-validation'
|
|
9
9
|
import { ensureTaskCompletionReport } from './task-reports'
|
|
10
10
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
11
11
|
import { executeSessionChatTurn } from './chat-execution'
|
|
12
12
|
import { extractTaskResult, formatResultBody } from './task-result'
|
|
13
13
|
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
14
|
-
import { isProtectedMainSession } from './main-session'
|
|
15
14
|
import { cascadeUnblock } from './dag-validation'
|
|
16
15
|
import { performGuardianRollback } from './guardian'
|
|
17
16
|
import type { Agent, BoardTask, Connector, Message } from '@/types'
|
|
@@ -282,10 +281,6 @@ function pushQueueUnique(queue: string[], id: string): void {
|
|
|
282
281
|
if (!queueContains(queue, id)) queue.push(id)
|
|
283
282
|
}
|
|
284
283
|
|
|
285
|
-
function isMainSession(session: SessionLike | null | undefined): boolean {
|
|
286
|
-
return isProtectedMainSession(session)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
284
|
function resolveTaskOwnerUser(task: ScheduleTaskMeta, sessions: Record<string, SessionLike>): string | null {
|
|
290
285
|
const direct = typeof task.user === 'string' ? task.user.trim() : ''
|
|
291
286
|
if (direct) return direct
|
|
@@ -521,10 +516,8 @@ async function executeTaskRun(
|
|
|
521
516
|
'- Include concrete evidence in your final summary: changed file paths, commands run, and verification results.',
|
|
522
517
|
'- If blocked, state the blocker explicitly and what input or permission is missing.',
|
|
523
518
|
].join('\n')
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
519
|
+
// All agents (including orchestrators) go through the unified chat execution path.
|
|
520
|
+
// Agents with subAgentIds get delegation tools automatically via session-tools.
|
|
528
521
|
const run = await executeSessionChatTurn({
|
|
529
522
|
sessionId,
|
|
530
523
|
message: prompt,
|
|
@@ -596,18 +589,7 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
596
589
|
return msg
|
|
597
590
|
}
|
|
598
591
|
|
|
599
|
-
|
|
600
|
-
if (!isMainSession(session)) continue
|
|
601
|
-
if (ownerUser && session?.user && session.user !== ownerUser) continue
|
|
602
|
-
const last = Array.isArray(session.messages) ? session.messages.at(-1) : null
|
|
603
|
-
if (last?.role === 'assistant' && last?.text === body && typeof last?.time === 'number' && now - last.time < 30_000) continue
|
|
604
|
-
if (!Array.isArray(session.messages)) session.messages = []
|
|
605
|
-
session.messages.push(buildMsg())
|
|
606
|
-
session.lastActiveAt = now
|
|
607
|
-
changed = true
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Also push to the agent's persistent thread session
|
|
592
|
+
// Push to the agent's shortcut chat session.
|
|
611
593
|
try {
|
|
612
594
|
const agents = loadAgents()
|
|
613
595
|
const agent = agents[task.agentId]
|
|
@@ -773,6 +755,7 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
773
755
|
if (task.claudeResumeId) resumeLines.push(`Claude session: \`${task.claudeResumeId}\``)
|
|
774
756
|
if (task.codexResumeId) resumeLines.push(`Codex thread: \`${task.codexResumeId}\``)
|
|
775
757
|
if (task.opencodeResumeId) resumeLines.push(`OpenCode session: \`${task.opencodeResumeId}\``)
|
|
758
|
+
if (task.geminiResumeId) resumeLines.push(`Gemini session: \`${task.geminiResumeId}\``)
|
|
776
759
|
// Fallback to legacy field
|
|
777
760
|
if (resumeLines.length === 0 && task.cliResumeId) {
|
|
778
761
|
resumeLines.push(`${task.cliProvider || 'CLI'} session: \`${task.cliResumeId}\``)
|
|
@@ -831,13 +814,12 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
831
814
|
}
|
|
832
815
|
|
|
833
816
|
// Push to delegating agent's active user-facing chat sessions
|
|
834
|
-
// so the result is visible in the chat the user is looking at
|
|
817
|
+
// so the result is visible in the chat the user is looking at.
|
|
835
818
|
if (delegator) {
|
|
836
819
|
for (const session of Object.values(sessions)) {
|
|
837
820
|
if (!session || session.agentId !== delegatedBy) continue
|
|
838
|
-
// Skip
|
|
821
|
+
// Skip the agent shortcut session itself.
|
|
839
822
|
if (session.id === delegator.threadSessionId) continue
|
|
840
|
-
if (session.sessionType === 'orchestrated') continue
|
|
841
823
|
// Only push to recently-active sessions (within last 30 minutes)
|
|
842
824
|
const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
|
|
843
825
|
if (now - lastActive > 30 * 60_000) continue
|
|
@@ -1315,24 +1297,27 @@ export async function processNext() {
|
|
|
1315
1297
|
const execSession = execSessions[sessionId] as Record<string, unknown> | undefined
|
|
1316
1298
|
if (execSession) {
|
|
1317
1299
|
const delegateIds = execSession.delegateResumeIds as
|
|
1318
|
-
| { claudeCode?: string | null; codex?: string | null; opencode?: string | null }
|
|
1300
|
+
| { claudeCode?: string | null; codex?: string | null; opencode?: string | null; gemini?: string | null }
|
|
1319
1301
|
| undefined
|
|
1320
1302
|
// Store each CLI resume ID separately
|
|
1321
1303
|
const claudeId = (execSession.claudeSessionId as string) || delegateIds?.claudeCode || null
|
|
1322
1304
|
const codexId = (execSession.codexThreadId as string) || delegateIds?.codex || null
|
|
1323
1305
|
const opencodeId = (execSession.opencodeSessionId as string) || delegateIds?.opencode || null
|
|
1306
|
+
const geminiId = delegateIds?.gemini || null
|
|
1324
1307
|
if (claudeId) t2[taskId].claudeResumeId = claudeId
|
|
1325
1308
|
if (codexId) t2[taskId].codexResumeId = codexId
|
|
1326
1309
|
if (opencodeId) t2[taskId].opencodeResumeId = opencodeId
|
|
1310
|
+
if (geminiId) t2[taskId].geminiResumeId = geminiId
|
|
1327
1311
|
// Keep backward-compat single field (first available)
|
|
1328
|
-
const primaryId = claudeId || codexId || opencodeId
|
|
1312
|
+
const primaryId = claudeId || codexId || opencodeId || geminiId
|
|
1329
1313
|
if (primaryId) {
|
|
1330
1314
|
t2[taskId].cliResumeId = primaryId
|
|
1331
1315
|
if (claudeId) t2[taskId].cliProvider = 'claude-cli'
|
|
1332
1316
|
else if (codexId) t2[taskId].cliProvider = 'codex-cli'
|
|
1333
1317
|
else if (opencodeId) t2[taskId].cliProvider = 'opencode-cli'
|
|
1318
|
+
else if (geminiId) t2[taskId].cliProvider = 'gemini-cli'
|
|
1334
1319
|
}
|
|
1335
|
-
console.log(`[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}`)
|
|
1320
|
+
console.log(`[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}, gemini=${geminiId}`)
|
|
1336
1321
|
}
|
|
1337
1322
|
} catch (e) {
|
|
1338
1323
|
console.warn(`[queue] Failed to extract CLI resume IDs for task ${taskId}:`, e)
|
|
@@ -6,6 +6,7 @@ import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
|
6
6
|
import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
|
|
7
7
|
import { enqueueSystemEvent } from './system-events'
|
|
8
8
|
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
9
|
+
import { processDueWatchJobs } from './watch-jobs'
|
|
9
10
|
|
|
10
11
|
const TICK_INTERVAL = 60_000 // 60 seconds
|
|
11
12
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
@@ -73,6 +74,7 @@ function computeNextRuns() {
|
|
|
73
74
|
|
|
74
75
|
async function tick() {
|
|
75
76
|
const now = Date.now()
|
|
77
|
+
await processDueWatchJobs(now)
|
|
76
78
|
const schedules = loadSchedules()
|
|
77
79
|
const agents = loadAgents()
|
|
78
80
|
const tasks = loadTasks()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import type { Session } from '@/types'
|
|
4
|
+
import { buildSessionArchiveMarkdown, buildSessionArchivePayload } from './session-archive-memory'
|
|
5
|
+
|
|
6
|
+
test('buildSessionArchivePayload summarizes session transcript and metadata', () => {
|
|
7
|
+
const session = {
|
|
8
|
+
id: 'session-1',
|
|
9
|
+
name: 'Support Thread',
|
|
10
|
+
cwd: process.cwd(),
|
|
11
|
+
user: 'Alice',
|
|
12
|
+
provider: 'openai',
|
|
13
|
+
model: 'gpt-4.1',
|
|
14
|
+
claudeSessionId: null,
|
|
15
|
+
codexThreadId: null,
|
|
16
|
+
opencodeSessionId: null,
|
|
17
|
+
createdAt: Date.parse('2026-03-05T00:00:00.000Z'),
|
|
18
|
+
lastActiveAt: Date.parse('2026-03-05T10:00:00.000Z'),
|
|
19
|
+
sessionType: 'human',
|
|
20
|
+
messages: [
|
|
21
|
+
{ role: 'user', text: 'Can you help me debug this issue?', time: 1 },
|
|
22
|
+
{ role: 'assistant', text: 'Yes, show me the stack trace.', time: 2, toolEvents: [{ name: 'files', input: '{}' }] },
|
|
23
|
+
],
|
|
24
|
+
identityState: { personaLabel: 'Debugger' },
|
|
25
|
+
} as Session
|
|
26
|
+
|
|
27
|
+
const payload = buildSessionArchivePayload(session, { name: 'Swarmy' })
|
|
28
|
+
|
|
29
|
+
assert.ok(payload)
|
|
30
|
+
assert.equal(payload?.title, 'Session archive: Support Thread')
|
|
31
|
+
assert.match(payload?.content || '', /Transcript excerpt:/)
|
|
32
|
+
assert.match(payload?.content || '', /Swarmy/)
|
|
33
|
+
assert.equal(payload?.metadata.tier, 'archive')
|
|
34
|
+
assert.equal(payload?.references[0]?.type, 'session')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('buildSessionArchiveMarkdown creates a portable markdown snapshot', () => {
|
|
38
|
+
const session = {
|
|
39
|
+
id: 'session-3',
|
|
40
|
+
name: 'Architecture Review',
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
user: 'Alice',
|
|
43
|
+
provider: 'openai',
|
|
44
|
+
model: 'gpt-4.1',
|
|
45
|
+
claudeSessionId: null,
|
|
46
|
+
codexThreadId: null,
|
|
47
|
+
opencodeSessionId: null,
|
|
48
|
+
createdAt: Date.parse('2026-03-05T00:00:00.000Z'),
|
|
49
|
+
lastActiveAt: Date.parse('2026-03-05T10:00:00.000Z'),
|
|
50
|
+
sessionType: 'human',
|
|
51
|
+
messages: [
|
|
52
|
+
{ role: 'user', text: 'Summarize the new connector policy.', time: 1 },
|
|
53
|
+
{ role: 'assistant', text: 'It now uses scoped sessions and freshness resets.', time: 2 },
|
|
54
|
+
],
|
|
55
|
+
identityState: { personaLabel: 'Reviewer' },
|
|
56
|
+
} as Session
|
|
57
|
+
|
|
58
|
+
const payload = buildSessionArchivePayload(session, { name: 'Swarmy' })
|
|
59
|
+
assert.ok(payload)
|
|
60
|
+
|
|
61
|
+
const markdown = buildSessionArchiveMarkdown(session, payload!, { name: 'Swarmy' })
|
|
62
|
+
assert.match(markdown, /^# Session archive: Architecture Review/m)
|
|
63
|
+
assert.match(markdown, /## Archive Snapshot/)
|
|
64
|
+
assert.match(markdown, /## Transcript Excerpt/)
|
|
65
|
+
assert.match(markdown, /\*\*Swarmy\*\*/)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('buildSessionArchivePayload skips trivial sessions', () => {
|
|
69
|
+
const session = {
|
|
70
|
+
id: 'session-2',
|
|
71
|
+
name: 'Too Short',
|
|
72
|
+
cwd: process.cwd(),
|
|
73
|
+
user: 'Bob',
|
|
74
|
+
provider: 'openai',
|
|
75
|
+
model: 'gpt-4.1',
|
|
76
|
+
claudeSessionId: null,
|
|
77
|
+
codexThreadId: null,
|
|
78
|
+
opencodeSessionId: null,
|
|
79
|
+
createdAt: 1,
|
|
80
|
+
lastActiveAt: 1,
|
|
81
|
+
messages: [{ role: 'user', text: 'hi', time: 1 }],
|
|
82
|
+
} as Session
|
|
83
|
+
|
|
84
|
+
assert.equal(buildSessionArchivePayload(session), null)
|
|
85
|
+
})
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createHash } from 'crypto'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import type { Agent, MemoryEntry, MemoryReference, Session } from '@/types'
|
|
5
|
+
import { getMemoryDb } from './memory-db'
|
|
6
|
+
import { loadAgents, loadSessions, saveSessions } from './storage'
|
|
7
|
+
import { DATA_DIR } from './data-dir'
|
|
8
|
+
|
|
9
|
+
const MAX_ARCHIVE_MESSAGES = 36
|
|
10
|
+
const MAX_ARCHIVE_LINE_CHARS = 320
|
|
11
|
+
const SESSION_ARCHIVE_EXPORT_DIR = path.join(DATA_DIR, 'session-archives')
|
|
12
|
+
|
|
13
|
+
function toOneLine(value: unknown, maxChars: number): string {
|
|
14
|
+
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, maxChars)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function messageSpeaker(session: Session, agent: Partial<Agent> | null | undefined, message: Session['messages'][number]): string {
|
|
18
|
+
if (message.role === 'assistant') return agent?.name || 'assistant'
|
|
19
|
+
return session.connectorContext?.senderName || session.user || 'user'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function slugifySegment(value: string, fallback: string): string {
|
|
23
|
+
const normalized = value
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '')
|
|
27
|
+
return normalized || fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildSessionArchivePayload(
|
|
31
|
+
session: Session,
|
|
32
|
+
agent?: Partial<Agent> | null,
|
|
33
|
+
): {
|
|
34
|
+
title: string
|
|
35
|
+
content: string
|
|
36
|
+
metadata: Record<string, unknown>
|
|
37
|
+
references: MemoryReference[]
|
|
38
|
+
hash: string
|
|
39
|
+
} | null {
|
|
40
|
+
if (!Array.isArray(session.messages) || session.messages.length < 2) return null
|
|
41
|
+
|
|
42
|
+
const excerpt = session.messages.slice(-MAX_ARCHIVE_MESSAGES).map((message) => {
|
|
43
|
+
const speaker = messageSpeaker(session, agent, message)
|
|
44
|
+
const kind = message.kind && message.kind !== 'chat' ? ` [${message.kind}]` : ''
|
|
45
|
+
const text = toOneLine(message.text, MAX_ARCHIVE_LINE_CHARS)
|
|
46
|
+
const tools = Array.isArray(message.toolEvents) && message.toolEvents.length > 0
|
|
47
|
+
? ` | tools=${message.toolEvents.map((event) => event.name).join(',')}`
|
|
48
|
+
: ''
|
|
49
|
+
return `- ${speaker}${kind}: ${text}${tools}`
|
|
50
|
+
}).join('\n')
|
|
51
|
+
|
|
52
|
+
const title = `Session archive: ${session.name || session.id}`
|
|
53
|
+
const content = [
|
|
54
|
+
`session_id: ${session.id}`,
|
|
55
|
+
`session_name: ${toOneLine(session.name, 160)}`,
|
|
56
|
+
`session_type: ${toOneLine(session.sessionType || 'human', 32)}`,
|
|
57
|
+
`agent_name: ${toOneLine(agent?.name || '', 80)}`,
|
|
58
|
+
`last_active_iso: ${new Date(session.lastActiveAt || Date.now()).toISOString()}`,
|
|
59
|
+
`message_count: ${session.messages.length}`,
|
|
60
|
+
session.identityState?.personaLabel ? `persona_label: ${toOneLine(session.identityState.personaLabel, 120)}` : '',
|
|
61
|
+
'',
|
|
62
|
+
'Transcript excerpt:',
|
|
63
|
+
excerpt,
|
|
64
|
+
].filter(Boolean).join('\n')
|
|
65
|
+
|
|
66
|
+
const hash = createHash('sha256').update(`${title}\n${content}`).digest('hex').slice(0, 16)
|
|
67
|
+
return {
|
|
68
|
+
title,
|
|
69
|
+
content,
|
|
70
|
+
metadata: {
|
|
71
|
+
tier: 'archive',
|
|
72
|
+
archiveHash: hash,
|
|
73
|
+
sessionName: session.name,
|
|
74
|
+
sessionType: session.sessionType || 'human',
|
|
75
|
+
messageCount: session.messages.length,
|
|
76
|
+
lastActiveAt: session.lastActiveAt || Date.now(),
|
|
77
|
+
personaLabel: session.identityState?.personaLabel || null,
|
|
78
|
+
},
|
|
79
|
+
references: [{
|
|
80
|
+
type: 'session',
|
|
81
|
+
path: session.id,
|
|
82
|
+
title: session.name,
|
|
83
|
+
note: 'Searchable session archive snapshot',
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
}],
|
|
86
|
+
hash,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildSessionArchiveMarkdown(
|
|
91
|
+
session: Session,
|
|
92
|
+
payload: NonNullable<ReturnType<typeof buildSessionArchivePayload>>,
|
|
93
|
+
agent?: Partial<Agent> | null,
|
|
94
|
+
): string {
|
|
95
|
+
const transcriptLines = session.messages.slice(-MAX_ARCHIVE_MESSAGES).map((message) => {
|
|
96
|
+
const speaker = messageSpeaker(session, agent, message)
|
|
97
|
+
const kind = message.kind && message.kind !== 'chat' ? ` (${message.kind})` : ''
|
|
98
|
+
const toolSummary = Array.isArray(message.toolEvents) && message.toolEvents.length > 0
|
|
99
|
+
? ` [tools: ${message.toolEvents.map((event) => event.name).join(', ')}]`
|
|
100
|
+
: ''
|
|
101
|
+
return `- **${speaker}**${kind}: ${toOneLine(message.text, MAX_ARCHIVE_LINE_CHARS)}${toolSummary}`
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
`# ${payload.title}`,
|
|
106
|
+
'',
|
|
107
|
+
`- Session ID: ${session.id}`,
|
|
108
|
+
`- Session Name: ${toOneLine(session.name, 160)}`,
|
|
109
|
+
`- Session Type: ${toOneLine(session.sessionType || 'human', 32)}`,
|
|
110
|
+
`- Agent: ${toOneLine(agent?.name || session.agentId || 'unknown', 80)}`,
|
|
111
|
+
`- Last Active: ${new Date(session.lastActiveAt || Date.now()).toISOString()}`,
|
|
112
|
+
`- Messages: ${session.messages.length}`,
|
|
113
|
+
session.identityState?.personaLabel ? `- Persona: ${toOneLine(session.identityState.personaLabel, 120)}` : '',
|
|
114
|
+
'',
|
|
115
|
+
'## Archive Snapshot',
|
|
116
|
+
'',
|
|
117
|
+
'```text',
|
|
118
|
+
payload.content,
|
|
119
|
+
'```',
|
|
120
|
+
'',
|
|
121
|
+
'## Transcript Excerpt',
|
|
122
|
+
'',
|
|
123
|
+
...transcriptLines,
|
|
124
|
+
'',
|
|
125
|
+
].filter(Boolean).join('\n')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function exportSessionArchiveMarkdown(
|
|
129
|
+
session: Session,
|
|
130
|
+
payload: NonNullable<ReturnType<typeof buildSessionArchivePayload>>,
|
|
131
|
+
agent?: Partial<Agent> | null,
|
|
132
|
+
): string | null {
|
|
133
|
+
try {
|
|
134
|
+
const agentSegment = slugifySegment(agent?.name || session.agentId || 'shared', 'shared')
|
|
135
|
+
const sessionSegment = slugifySegment(session.name || session.id, session.id)
|
|
136
|
+
const dir = path.join(SESSION_ARCHIVE_EXPORT_DIR, agentSegment)
|
|
137
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
138
|
+
const filePath = path.join(dir, `${sessionSegment}-${session.id}.md`)
|
|
139
|
+
fs.writeFileSync(filePath, buildSessionArchiveMarkdown(session, payload, agent))
|
|
140
|
+
return filePath
|
|
141
|
+
} catch {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function syncSessionArchiveMemory(
|
|
147
|
+
session: Session,
|
|
148
|
+
opts?: { agent?: Partial<Agent> | null },
|
|
149
|
+
): { stored: boolean; memoryId?: string; reason?: string } {
|
|
150
|
+
const agent = opts?.agent ?? (session.agentId ? loadAgents()[session.agentId] : null)
|
|
151
|
+
if (!session.agentId && !agent?.id) {
|
|
152
|
+
return { stored: false, reason: 'missing_agent' }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const payload = buildSessionArchivePayload(session, agent)
|
|
156
|
+
if (!payload) {
|
|
157
|
+
return { stored: false, reason: 'insufficient_messages' }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const memDb = getMemoryDb()
|
|
161
|
+
const existing = memDb.getLatestBySessionCategory(session.id, 'session_archive')
|
|
162
|
+
const existingHash = typeof existing?.metadata?.archiveHash === 'string'
|
|
163
|
+
? existing.metadata.archiveHash
|
|
164
|
+
: null
|
|
165
|
+
if (session.sessionArchiveState?.lastHash === payload.hash || existingHash === payload.hash) {
|
|
166
|
+
session.sessionArchiveState = {
|
|
167
|
+
memoryId: session.sessionArchiveState?.memoryId || existing?.id || null,
|
|
168
|
+
lastHash: payload.hash,
|
|
169
|
+
lastSyncedAt: session.sessionArchiveState?.lastSyncedAt || existing?.updatedAt || null,
|
|
170
|
+
messageCount: session.messages.length,
|
|
171
|
+
exportPath: session.sessionArchiveState?.exportPath || null,
|
|
172
|
+
}
|
|
173
|
+
return { stored: false, memoryId: existing?.id || session.sessionArchiveState.memoryId || undefined, reason: 'unchanged' }
|
|
174
|
+
}
|
|
175
|
+
const entry: MemoryEntry | null = existing
|
|
176
|
+
? memDb.update(existing.id, {
|
|
177
|
+
title: payload.title,
|
|
178
|
+
content: payload.content,
|
|
179
|
+
metadata: payload.metadata,
|
|
180
|
+
references: payload.references,
|
|
181
|
+
linkedMemoryIds: existing.linkedMemoryIds,
|
|
182
|
+
})
|
|
183
|
+
: memDb.add({
|
|
184
|
+
agentId: session.agentId || agent?.id || null,
|
|
185
|
+
sessionId: session.id,
|
|
186
|
+
category: 'session_archive',
|
|
187
|
+
title: payload.title,
|
|
188
|
+
content: payload.content,
|
|
189
|
+
metadata: payload.metadata,
|
|
190
|
+
references: payload.references,
|
|
191
|
+
linkedMemoryIds: [],
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
if (!entry) return { stored: false, reason: 'store_failed' }
|
|
195
|
+
const exportPath = exportSessionArchiveMarkdown(session, payload, agent)
|
|
196
|
+
|
|
197
|
+
session.sessionArchiveState = {
|
|
198
|
+
memoryId: entry.id,
|
|
199
|
+
lastHash: payload.hash,
|
|
200
|
+
lastSyncedAt: Date.now(),
|
|
201
|
+
messageCount: session.messages.length,
|
|
202
|
+
exportPath,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { stored: true, memoryId: entry.id }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function syncAllSessionArchiveMemories(): { synced: number; skipped: number; sessionIds: string[] } {
|
|
209
|
+
const sessions = loadSessions()
|
|
210
|
+
const agents = loadAgents()
|
|
211
|
+
let changed = false
|
|
212
|
+
let synced = 0
|
|
213
|
+
let skipped = 0
|
|
214
|
+
const sessionIds: string[] = []
|
|
215
|
+
|
|
216
|
+
for (const session of Object.values(sessions) as Session[]) {
|
|
217
|
+
const agent = session.agentId ? agents[session.agentId] : null
|
|
218
|
+
const result = syncSessionArchiveMemory(session, { agent })
|
|
219
|
+
if (result.stored) {
|
|
220
|
+
synced += 1
|
|
221
|
+
sessionIds.push(session.id)
|
|
222
|
+
changed = true
|
|
223
|
+
} else {
|
|
224
|
+
skipped += 1
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (changed) saveSessions(sessions)
|
|
229
|
+
return { synced, skipped, sessionIds }
|
|
230
|
+
}
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
|
+
import type { MailboxEnvelope } from '@/types'
|
|
2
3
|
import { loadSessions, saveSessions } from './storage'
|
|
3
4
|
|
|
4
|
-
export type MailboxStatus = 'new' | 'ack'
|
|
5
|
-
|
|
6
|
-
export interface MailboxEnvelope {
|
|
7
|
-
id: string
|
|
8
|
-
type: string
|
|
9
|
-
payload: string
|
|
10
|
-
fromSessionId?: string | null
|
|
11
|
-
fromAgentId?: string | null
|
|
12
|
-
toSessionId: string
|
|
13
|
-
toAgentId?: string | null
|
|
14
|
-
correlationId?: string | null
|
|
15
|
-
status: MailboxStatus
|
|
16
|
-
createdAt: number
|
|
17
|
-
expiresAt?: number | null
|
|
18
|
-
ackAt?: number | null
|
|
19
|
-
}
|
|
20
|
-
|
|
21
5
|
interface MailboxOptions {
|
|
22
6
|
limit?: number
|
|
23
7
|
includeAcked?: boolean
|
|
@@ -78,6 +62,13 @@ export function sendMailboxEnvelope(input: {
|
|
|
78
62
|
target.lastActiveAt = now
|
|
79
63
|
sessions[input.toSessionId] = target
|
|
80
64
|
saveSessions(sessions)
|
|
65
|
+
import('./watch-jobs')
|
|
66
|
+
.then(({ triggerMailboxWatchJobs }) => {
|
|
67
|
+
triggerMailboxWatchJobs({ sessionId: input.toSessionId, envelope })
|
|
68
|
+
})
|
|
69
|
+
.catch(() => {
|
|
70
|
+
// best-effort trigger only
|
|
71
|
+
})
|
|
81
72
|
return envelope
|
|
82
73
|
}
|
|
83
74
|
|
|
@@ -126,4 +117,3 @@ export function clearMailbox(sessionId: string, includeAcked = true): { before:
|
|
|
126
117
|
saveSessions(sessions)
|
|
127
118
|
return { before, after: afterList.length }
|
|
128
119
|
}
|
|
129
|
-
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import type { Session } from '@/types'
|
|
4
|
+
import {
|
|
5
|
+
evaluateSessionFreshness,
|
|
6
|
+
inferSessionResetType,
|
|
7
|
+
resetSessionRuntime,
|
|
8
|
+
resolveSessionResetPolicy,
|
|
9
|
+
} from './session-reset-policy'
|
|
10
|
+
|
|
11
|
+
function makeSession(overrides: Partial<Session> = {}): Session {
|
|
12
|
+
return {
|
|
13
|
+
id: 's1',
|
|
14
|
+
name: 'Test Session',
|
|
15
|
+
cwd: process.cwd(),
|
|
16
|
+
user: 'user',
|
|
17
|
+
provider: 'openai',
|
|
18
|
+
model: 'gpt-4.1',
|
|
19
|
+
claudeSessionId: null,
|
|
20
|
+
codexThreadId: null,
|
|
21
|
+
opencodeSessionId: null,
|
|
22
|
+
messages: [{ role: 'user', text: 'hello', time: 1 }],
|
|
23
|
+
createdAt: 1,
|
|
24
|
+
lastActiveAt: 1,
|
|
25
|
+
...overrides,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('inferSessionResetType distinguishes direct, group, and thread sessions', () => {
|
|
30
|
+
assert.equal(inferSessionResetType(makeSession()), 'direct')
|
|
31
|
+
assert.equal(inferSessionResetType(makeSession({ connectorContext: { isGroup: true } })), 'group')
|
|
32
|
+
assert.equal(inferSessionResetType(makeSession({ connectorContext: { threadId: 'thread-1' } })), 'thread')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('resolveSessionResetPolicy falls back to type defaults', () => {
|
|
36
|
+
const direct = resolveSessionResetPolicy({ session: makeSession() })
|
|
37
|
+
assert.equal(direct.mode, 'idle')
|
|
38
|
+
assert.equal(direct.idleTimeoutSec, 12 * 60 * 60)
|
|
39
|
+
|
|
40
|
+
const thread = resolveSessionResetPolicy({ session: makeSession({ connectorContext: { threadId: 'thread-1' } }) })
|
|
41
|
+
assert.equal(thread.mode, 'idle')
|
|
42
|
+
assert.equal(thread.idleTimeoutSec, 4 * 60 * 60)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('evaluateSessionFreshness expires idle sessions', () => {
|
|
46
|
+
const session = makeSession({ createdAt: 0, lastActiveAt: 0 })
|
|
47
|
+
const policy = resolveSessionResetPolicy({
|
|
48
|
+
session: { ...session, sessionIdleTimeoutSec: 10, sessionMaxAgeSec: 60 },
|
|
49
|
+
})
|
|
50
|
+
const freshness = evaluateSessionFreshness({ session, policy, now: 11_000 })
|
|
51
|
+
assert.deepEqual(freshness.reason, 'idle_timeout:10')
|
|
52
|
+
assert.equal(freshness.fresh, false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('evaluateSessionFreshness supports daily reset boundaries', () => {
|
|
56
|
+
const session = makeSession({
|
|
57
|
+
createdAt: Date.parse('2026-03-04T00:00:00.000Z'),
|
|
58
|
+
lastActiveAt: Date.parse('2026-03-05T03:30:00.000Z'),
|
|
59
|
+
})
|
|
60
|
+
const policy = resolveSessionResetPolicy({
|
|
61
|
+
session: {
|
|
62
|
+
...session,
|
|
63
|
+
sessionResetMode: 'daily',
|
|
64
|
+
sessionDailyResetAt: '04:00',
|
|
65
|
+
sessionResetTimezone: 'UTC',
|
|
66
|
+
sessionMaxAgeSec: 999999,
|
|
67
|
+
sessionIdleTimeoutSec: 0,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
const freshness = evaluateSessionFreshness({
|
|
71
|
+
session,
|
|
72
|
+
policy,
|
|
73
|
+
now: Date.parse('2026-03-05T10:00:00.000Z'),
|
|
74
|
+
})
|
|
75
|
+
assert.equal(freshness.fresh, false)
|
|
76
|
+
assert.equal(freshness.reason, 'daily_reset:04:00')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('resetSessionRuntime clears transient state but preserves continuity state', () => {
|
|
80
|
+
const session = makeSession({
|
|
81
|
+
claudeSessionId: 'claude',
|
|
82
|
+
codexThreadId: 'codex',
|
|
83
|
+
opencodeSessionId: 'open',
|
|
84
|
+
delegateResumeIds: { claudeCode: 'a', codex: 'b', opencode: 'c', gemini: 'd' },
|
|
85
|
+
lastHeartbeatText: 'heartbeat',
|
|
86
|
+
lastHeartbeatSentAt: 123,
|
|
87
|
+
lastAutoMemoryAt: 456,
|
|
88
|
+
conversationTone: 'formal',
|
|
89
|
+
identityState: { personaLabel: 'Planner' },
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const cleared = resetSessionRuntime(session, 'idle_timeout:10', { now: 1000 })
|
|
93
|
+
|
|
94
|
+
assert.equal(cleared, 1)
|
|
95
|
+
assert.deepEqual(session.messages, [])
|
|
96
|
+
assert.equal(session.claudeSessionId, null)
|
|
97
|
+
assert.equal(session.identityState?.personaLabel, 'Planner')
|
|
98
|
+
assert.equal(session.lastSessionResetReason, 'idle_timeout:10')
|
|
99
|
+
})
|