@swarmclawai/swarmclaw 0.7.2 → 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 +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- 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 +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- 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/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- 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 +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- 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 +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- 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 +245 -46
- 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 +74 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- 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/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- 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/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 +250 -61
- 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 +45 -5
- 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 +946 -110
- 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/daemon-state.ts +59 -1
- 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 +13 -39
- 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 +27 -967
- 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 +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- 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 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +70 -32
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery.ts +22 -4
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- 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 +237 -24
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +86 -23
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- 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 +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- 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/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import type { Agent, AppSettings, Session, SessionResetMode, SessionResetType } from '@/types'
|
|
2
|
+
|
|
3
|
+
export interface ResolvedSessionResetPolicy {
|
|
4
|
+
type: SessionResetType
|
|
5
|
+
mode: SessionResetMode
|
|
6
|
+
idleTimeoutSec: number | null
|
|
7
|
+
maxAgeSec: number | null
|
|
8
|
+
dailyResetAt: string | null
|
|
9
|
+
timezone: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SessionFreshnessSnapshot {
|
|
13
|
+
fresh: boolean
|
|
14
|
+
reason?: string
|
|
15
|
+
policy: ResolvedSessionResetPolicy
|
|
16
|
+
idleExpiresAt: number | null
|
|
17
|
+
dailyBoundaryKey: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_POLICIES: Record<SessionResetType, ResolvedSessionResetPolicy> = {
|
|
21
|
+
direct: {
|
|
22
|
+
type: 'direct',
|
|
23
|
+
mode: 'idle',
|
|
24
|
+
idleTimeoutSec: 12 * 60 * 60,
|
|
25
|
+
maxAgeSec: 7 * 24 * 60 * 60,
|
|
26
|
+
dailyResetAt: null,
|
|
27
|
+
timezone: null,
|
|
28
|
+
},
|
|
29
|
+
group: {
|
|
30
|
+
type: 'group',
|
|
31
|
+
mode: 'idle',
|
|
32
|
+
idleTimeoutSec: 6 * 60 * 60,
|
|
33
|
+
maxAgeSec: 3 * 24 * 60 * 60,
|
|
34
|
+
dailyResetAt: null,
|
|
35
|
+
timezone: null,
|
|
36
|
+
},
|
|
37
|
+
thread: {
|
|
38
|
+
type: 'thread',
|
|
39
|
+
mode: 'idle',
|
|
40
|
+
idleTimeoutSec: 4 * 60 * 60,
|
|
41
|
+
maxAgeSec: 2 * 24 * 60 * 60,
|
|
42
|
+
dailyResetAt: null,
|
|
43
|
+
timezone: null,
|
|
44
|
+
},
|
|
45
|
+
main: {
|
|
46
|
+
type: 'main',
|
|
47
|
+
mode: 'daily',
|
|
48
|
+
idleTimeoutSec: 24 * 60 * 60,
|
|
49
|
+
maxAgeSec: 14 * 24 * 60 * 60,
|
|
50
|
+
dailyResetAt: '04:00',
|
|
51
|
+
timezone: null,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseIntBounded(value: unknown, min: number, max: number): number | null {
|
|
56
|
+
if (value === null || value === undefined || value === '') return null
|
|
57
|
+
const parsed = typeof value === 'number'
|
|
58
|
+
? value
|
|
59
|
+
: typeof value === 'string'
|
|
60
|
+
? Number.parseInt(value.trim(), 10)
|
|
61
|
+
: Number.NaN
|
|
62
|
+
if (!Number.isFinite(parsed)) return null
|
|
63
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeMode(raw: unknown, fallback: SessionResetMode): SessionResetMode {
|
|
67
|
+
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
|
68
|
+
return value === 'daily' ? 'daily' : value === 'idle' ? 'idle' : fallback
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeTimeHHMM(raw: unknown): string | null {
|
|
72
|
+
if (typeof raw !== 'string') return null
|
|
73
|
+
const value = raw.trim()
|
|
74
|
+
const match = value.match(/^(\d{1,2}):(\d{2})$/)
|
|
75
|
+
if (!match) return null
|
|
76
|
+
const hours = Number.parseInt(match[1], 10)
|
|
77
|
+
const minutes = Number.parseInt(match[2], 10)
|
|
78
|
+
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null
|
|
79
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null
|
|
80
|
+
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeTimezone(raw: unknown): string | null {
|
|
84
|
+
if (typeof raw !== 'string') return null
|
|
85
|
+
const value = raw.trim()
|
|
86
|
+
return value || null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getClockParts(date: Date, timezone?: string | null): { dateKey: string; minutes: number } | null {
|
|
90
|
+
try {
|
|
91
|
+
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
92
|
+
year: 'numeric',
|
|
93
|
+
month: '2-digit',
|
|
94
|
+
day: '2-digit',
|
|
95
|
+
hour: '2-digit',
|
|
96
|
+
minute: '2-digit',
|
|
97
|
+
hour12: false,
|
|
98
|
+
timeZone: timezone || undefined,
|
|
99
|
+
})
|
|
100
|
+
const parts = formatter.formatToParts(date)
|
|
101
|
+
const year = parts.find((part) => part.type === 'year')?.value
|
|
102
|
+
const month = parts.find((part) => part.type === 'month')?.value
|
|
103
|
+
const day = parts.find((part) => part.type === 'day')?.value
|
|
104
|
+
const hour = Number.parseInt(parts.find((part) => part.type === 'hour')?.value || '', 10)
|
|
105
|
+
const minute = Number.parseInt(parts.find((part) => part.type === 'minute')?.value || '', 10)
|
|
106
|
+
if (!year || !month || !day || !Number.isFinite(hour) || !Number.isFinite(minute)) return null
|
|
107
|
+
return {
|
|
108
|
+
dateKey: `${year}-${month}-${day}`,
|
|
109
|
+
minutes: hour * 60 + minute,
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function boundaryKeyForNow(now: number, boundaryMinutes: number, timezone?: string | null): string | null {
|
|
117
|
+
const current = getClockParts(new Date(now), timezone)
|
|
118
|
+
if (!current) return null
|
|
119
|
+
if (current.minutes >= boundaryMinutes) return current.dateKey
|
|
120
|
+
const previous = getClockParts(new Date(now - 24 * 60 * 60 * 1000), timezone)
|
|
121
|
+
return previous?.dateKey || null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function rawField(
|
|
125
|
+
session: Partial<Session> | null | undefined,
|
|
126
|
+
overrides: Record<string, unknown> | undefined,
|
|
127
|
+
agent: Partial<Agent> | null | undefined,
|
|
128
|
+
settings: Partial<AppSettings> | null | undefined,
|
|
129
|
+
key: 'sessionResetMode' | 'sessionIdleTimeoutSec' | 'sessionMaxAgeSec' | 'sessionDailyResetAt' | 'sessionResetTimezone',
|
|
130
|
+
): unknown {
|
|
131
|
+
if (session && session[key] !== undefined) return session[key]
|
|
132
|
+
if (overrides && overrides[key] !== undefined) return overrides[key]
|
|
133
|
+
if (agent && agent[key] !== undefined) return agent[key]
|
|
134
|
+
if (settings && settings[key] !== undefined) return settings[key]
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function inferSessionResetType(
|
|
139
|
+
session: Partial<Session> | null | undefined,
|
|
140
|
+
opts?: { isGroup?: boolean | null; threadId?: string | null },
|
|
141
|
+
): SessionResetType {
|
|
142
|
+
if ((session?.sessionType as string | undefined) === 'orchestrated') return 'main'
|
|
143
|
+
const threadId = opts?.threadId ?? session?.connectorContext?.threadId ?? null
|
|
144
|
+
if (threadId) return 'thread'
|
|
145
|
+
const isGroup = opts?.isGroup ?? session?.connectorContext?.isGroup ?? false
|
|
146
|
+
return isGroup ? 'group' : 'direct'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function resolveSessionResetPolicy(params: {
|
|
150
|
+
session?: Partial<Session> | null
|
|
151
|
+
agent?: Partial<Agent> | null
|
|
152
|
+
settings?: Partial<AppSettings> | null
|
|
153
|
+
resetType?: SessionResetType
|
|
154
|
+
overrides?: Record<string, unknown>
|
|
155
|
+
}): ResolvedSessionResetPolicy {
|
|
156
|
+
const type = params.resetType ?? inferSessionResetType(params.session)
|
|
157
|
+
const defaults = DEFAULT_POLICIES[type]
|
|
158
|
+
return {
|
|
159
|
+
type,
|
|
160
|
+
mode: normalizeMode(
|
|
161
|
+
rawField(params.session, params.overrides, params.agent, params.settings, 'sessionResetMode'),
|
|
162
|
+
defaults.mode,
|
|
163
|
+
),
|
|
164
|
+
idleTimeoutSec: parseIntBounded(
|
|
165
|
+
rawField(params.session, params.overrides, params.agent, params.settings, 'sessionIdleTimeoutSec'),
|
|
166
|
+
0,
|
|
167
|
+
180 * 24 * 60 * 60,
|
|
168
|
+
) ?? defaults.idleTimeoutSec,
|
|
169
|
+
maxAgeSec: parseIntBounded(
|
|
170
|
+
rawField(params.session, params.overrides, params.agent, params.settings, 'sessionMaxAgeSec'),
|
|
171
|
+
0,
|
|
172
|
+
365 * 24 * 60 * 60,
|
|
173
|
+
) ?? defaults.maxAgeSec,
|
|
174
|
+
dailyResetAt: normalizeTimeHHMM(
|
|
175
|
+
rawField(params.session, params.overrides, params.agent, params.settings, 'sessionDailyResetAt'),
|
|
176
|
+
) ?? defaults.dailyResetAt,
|
|
177
|
+
timezone: normalizeTimezone(
|
|
178
|
+
rawField(params.session, params.overrides, params.agent, params.settings, 'sessionResetTimezone'),
|
|
179
|
+
) ?? defaults.timezone,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function evaluateSessionFreshness(params: {
|
|
184
|
+
session?: Partial<Session> | null
|
|
185
|
+
policy: ResolvedSessionResetPolicy
|
|
186
|
+
now?: number
|
|
187
|
+
}): SessionFreshnessSnapshot {
|
|
188
|
+
const now = typeof params.now === 'number' ? params.now : Date.now()
|
|
189
|
+
const session = params.session
|
|
190
|
+
const policy = params.policy
|
|
191
|
+
const messageCount = Array.isArray(session?.messages) ? session.messages.length : 0
|
|
192
|
+
const createdAt = typeof session?.createdAt === 'number' ? session.createdAt : now
|
|
193
|
+
const lastActiveAt = typeof session?.lastActiveAt === 'number' ? session.lastActiveAt : createdAt
|
|
194
|
+
const idleExpiresAt = typeof policy.idleTimeoutSec === 'number' && policy.idleTimeoutSec > 0
|
|
195
|
+
? lastActiveAt + policy.idleTimeoutSec * 1000
|
|
196
|
+
: null
|
|
197
|
+
|
|
198
|
+
if (!session || messageCount === 0) {
|
|
199
|
+
return {
|
|
200
|
+
fresh: true,
|
|
201
|
+
policy,
|
|
202
|
+
idleExpiresAt,
|
|
203
|
+
dailyBoundaryKey: null,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (idleExpiresAt !== null && now > idleExpiresAt) {
|
|
208
|
+
return {
|
|
209
|
+
fresh: false,
|
|
210
|
+
reason: `idle_timeout:${policy.idleTimeoutSec}`,
|
|
211
|
+
policy,
|
|
212
|
+
idleExpiresAt,
|
|
213
|
+
dailyBoundaryKey: null,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof policy.maxAgeSec === 'number' && policy.maxAgeSec > 0) {
|
|
218
|
+
const maxAgeMs = policy.maxAgeSec * 1000
|
|
219
|
+
if (now - createdAt > maxAgeMs) {
|
|
220
|
+
return {
|
|
221
|
+
fresh: false,
|
|
222
|
+
reason: `max_age:${policy.maxAgeSec}`,
|
|
223
|
+
policy,
|
|
224
|
+
idleExpiresAt,
|
|
225
|
+
dailyBoundaryKey: null,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (policy.mode === 'daily' && policy.dailyResetAt) {
|
|
231
|
+
const boundary = normalizeTimeHHMM(policy.dailyResetAt)
|
|
232
|
+
if (boundary) {
|
|
233
|
+
const [hours, minutes] = boundary.split(':').map((value) => Number.parseInt(value, 10))
|
|
234
|
+
const boundaryMinutes = hours * 60 + minutes
|
|
235
|
+
const nowBoundaryKey = boundaryKeyForNow(now, boundaryMinutes, policy.timezone)
|
|
236
|
+
const lastActiveParts = getClockParts(new Date(lastActiveAt), policy.timezone)
|
|
237
|
+
if (
|
|
238
|
+
nowBoundaryKey
|
|
239
|
+
&& lastActiveParts
|
|
240
|
+
&& (
|
|
241
|
+
lastActiveParts.dateKey < nowBoundaryKey
|
|
242
|
+
|| (lastActiveParts.dateKey === nowBoundaryKey && lastActiveParts.minutes < boundaryMinutes)
|
|
243
|
+
)
|
|
244
|
+
) {
|
|
245
|
+
return {
|
|
246
|
+
fresh: false,
|
|
247
|
+
reason: `daily_reset:${policy.dailyResetAt}`,
|
|
248
|
+
policy,
|
|
249
|
+
idleExpiresAt,
|
|
250
|
+
dailyBoundaryKey: nowBoundaryKey,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
fresh: true,
|
|
255
|
+
policy,
|
|
256
|
+
idleExpiresAt,
|
|
257
|
+
dailyBoundaryKey: nowBoundaryKey,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
fresh: true,
|
|
264
|
+
policy,
|
|
265
|
+
idleExpiresAt,
|
|
266
|
+
dailyBoundaryKey: null,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function resetSessionRuntime(
|
|
271
|
+
session: Session,
|
|
272
|
+
reason: string,
|
|
273
|
+
opts?: { now?: number },
|
|
274
|
+
): number {
|
|
275
|
+
const now = typeof opts?.now === 'number' ? opts.now : Date.now()
|
|
276
|
+
const cleared = Array.isArray(session.messages) ? session.messages.length : 0
|
|
277
|
+
|
|
278
|
+
session.messages = []
|
|
279
|
+
session.claudeSessionId = null
|
|
280
|
+
session.codexThreadId = null
|
|
281
|
+
session.opencodeSessionId = null
|
|
282
|
+
session.delegateResumeIds = {
|
|
283
|
+
claudeCode: null,
|
|
284
|
+
codex: null,
|
|
285
|
+
opencode: null,
|
|
286
|
+
gemini: null,
|
|
287
|
+
}
|
|
288
|
+
session.createdAt = now
|
|
289
|
+
session.lastActiveAt = now
|
|
290
|
+
session.lastAutoMemoryAt = null
|
|
291
|
+
session.lastHeartbeatText = null
|
|
292
|
+
session.lastHeartbeatSentAt = null
|
|
293
|
+
session.conversationTone = undefined
|
|
294
|
+
session.lastSessionResetAt = now
|
|
295
|
+
session.lastSessionResetReason = reason
|
|
296
|
+
|
|
297
|
+
if (session.connectorContext) {
|
|
298
|
+
session.connectorContext = {
|
|
299
|
+
...session.connectorContext,
|
|
300
|
+
lastResetAt: now,
|
|
301
|
+
lastResetReason: reason,
|
|
302
|
+
lastInboundMessageId: null,
|
|
303
|
+
lastInboundReplyToMessageId: null,
|
|
304
|
+
lastInboundThreadId: null,
|
|
305
|
+
lastOutboundMessageId: null,
|
|
306
|
+
lastOutboundAt: null,
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return cleared
|
|
311
|
+
}
|
|
@@ -4,7 +4,9 @@ import { active, loadSessions } from './storage'
|
|
|
4
4
|
import { executeSessionChatTurn, type ExecuteChatTurnResult } from './chat-execution'
|
|
5
5
|
import { loadRuntimeSettings } from './runtime-settings'
|
|
6
6
|
import { log } from './logger'
|
|
7
|
-
import {
|
|
7
|
+
import { isInternalHeartbeatRun } from './heartbeat-source'
|
|
8
|
+
import { cleanupSessionBrowser } from './session-tools/web'
|
|
9
|
+
import { cancelDelegationJobsForParentSession } from './delegation-jobs'
|
|
8
10
|
|
|
9
11
|
export type SessionRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
10
12
|
export type SessionQueueMode = 'followup' | 'steer' | 'collect'
|
|
@@ -122,6 +124,23 @@ function emitRunMeta(entry: QueueEntry, status: SessionRunStatus, extra?: Record
|
|
|
122
124
|
})
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
function markRunningEntryCancelled(entry: QueueEntry, reason: string) {
|
|
128
|
+
if (entry.run.status === 'cancelled') return
|
|
129
|
+
entry.run.status = 'cancelled'
|
|
130
|
+
entry.run.endedAt = now()
|
|
131
|
+
entry.run.error = reason
|
|
132
|
+
emitRunMeta(entry, 'cancelled', { reason })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function abortSessionRuntime(entry: QueueEntry, reason: string) {
|
|
136
|
+
markRunningEntryCancelled(entry, reason)
|
|
137
|
+
entry.signalController.abort()
|
|
138
|
+
try { active.get(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
|
|
139
|
+
active.delete(entry.run.sessionId)
|
|
140
|
+
try { cleanupSessionBrowser(entry.run.sessionId) } catch { /* noop */ }
|
|
141
|
+
try { cancelDelegationJobsForParentSession(entry.run.sessionId, reason) } catch { /* noop */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
function executionKeyForSession(sessionId: string): string {
|
|
126
145
|
return `session:${sessionId}`
|
|
127
146
|
}
|
|
@@ -170,7 +189,7 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
|
|
|
170
189
|
if (!queue.length) continue
|
|
171
190
|
const keep: QueueEntry[] = []
|
|
172
191
|
for (const entry of queue) {
|
|
173
|
-
const isHeartbeat = entry.run.internal
|
|
192
|
+
const isHeartbeat = isInternalHeartbeatRun(entry.run.internal, entry.run.source)
|
|
174
193
|
if (!isHeartbeat) {
|
|
175
194
|
keep.push(entry)
|
|
176
195
|
continue
|
|
@@ -187,45 +206,15 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
|
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
for (const entry of state.runningByExecution.values()) {
|
|
190
|
-
const isHeartbeat = entry.run.internal
|
|
209
|
+
const isHeartbeat = isInternalHeartbeatRun(entry.run.internal, entry.run.source)
|
|
191
210
|
if (!isHeartbeat) continue
|
|
192
211
|
abortedRunning += 1
|
|
193
|
-
entry
|
|
194
|
-
try { active.get(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
|
|
212
|
+
abortSessionRuntime(entry, reason)
|
|
195
213
|
}
|
|
196
214
|
|
|
197
215
|
return { cancelledQueued, abortedRunning }
|
|
198
216
|
}
|
|
199
217
|
|
|
200
|
-
function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupRequest) {
|
|
201
|
-
const delayMs = Math.max(0, Math.trunc(followup.delayMs || 0))
|
|
202
|
-
setTimeout(() => {
|
|
203
|
-
try {
|
|
204
|
-
const sessions = loadSessions()
|
|
205
|
-
const session = sessions[sessionId]
|
|
206
|
-
if (!session || !isMainMissionSession(session)) return
|
|
207
|
-
enqueueSessionRun({
|
|
208
|
-
sessionId,
|
|
209
|
-
message: followup.message,
|
|
210
|
-
internal: true,
|
|
211
|
-
source: 'main-loop-followup',
|
|
212
|
-
mode: 'collect',
|
|
213
|
-
dedupeKey: followup.dedupeKey,
|
|
214
|
-
})
|
|
215
|
-
} catch (err: any) {
|
|
216
|
-
log.warn('session-run', `Failed to enqueue main-loop followup for ${sessionId}`, err?.message || String(err))
|
|
217
|
-
}
|
|
218
|
-
}, delayMs)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function isMainMissionSession(session: Record<string, unknown>): boolean {
|
|
222
|
-
const id = typeof session.id === 'string' ? session.id.trim() : ''
|
|
223
|
-
const sessionType = typeof session.sessionType === 'string' ? session.sessionType : ''
|
|
224
|
-
if (id.startsWith('agent-thread-')) return true
|
|
225
|
-
if (sessionType === 'orchestrated') return true
|
|
226
|
-
return false
|
|
227
|
-
}
|
|
228
|
-
|
|
229
218
|
async function drainExecution(executionKey: string): Promise<void> {
|
|
230
219
|
if (state.runningByExecution.has(executionKey)) return
|
|
231
220
|
const q = queueForExecution(executionKey)
|
|
@@ -269,49 +258,25 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
269
258
|
})
|
|
270
259
|
|
|
271
260
|
const failed = !!result.error
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
message: next.message,
|
|
277
|
-
internal: next.run.internal,
|
|
278
|
-
source: next.run.source,
|
|
279
|
-
resultText: result.text,
|
|
280
|
-
error: result.error,
|
|
281
|
-
toolEvents: result.toolEvents,
|
|
282
|
-
inputTokens: result.inputTokens,
|
|
283
|
-
outputTokens: result.outputTokens,
|
|
284
|
-
estimatedCost: result.estimatedCost,
|
|
285
|
-
})
|
|
286
|
-
} catch (mainLoopErr: any) {
|
|
287
|
-
log.warn('session-run', `Main-loop update failed for ${next.run.id}`, mainLoopErr?.message || String(mainLoopErr))
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
next.run.status = failed ? 'failed' : 'completed'
|
|
291
|
-
next.run.endedAt = now()
|
|
292
|
-
next.run.error = result.error
|
|
261
|
+
const aborted = next.signalController.signal.aborted
|
|
262
|
+
next.run.status = aborted ? 'cancelled' : (failed ? 'failed' : 'completed')
|
|
263
|
+
next.run.endedAt = next.run.endedAt || now()
|
|
264
|
+
next.run.error = aborted ? (next.run.error || 'Cancelled') : result.error
|
|
293
265
|
next.run.resultPreview = result.text?.slice(0, 280)
|
|
294
266
|
emitRunMeta(next, next.run.status, {
|
|
295
267
|
persisted: result.persisted,
|
|
296
268
|
hasText: !!result.text,
|
|
297
|
-
error:
|
|
269
|
+
error: next.run.error || null,
|
|
298
270
|
})
|
|
299
271
|
log.info('session-run', `Run finished ${next.run.id}`, {
|
|
300
272
|
sessionId: next.run.sessionId,
|
|
301
273
|
status: next.run.status,
|
|
302
274
|
persisted: result.persisted,
|
|
303
275
|
hasText: !!result.text,
|
|
304
|
-
error:
|
|
276
|
+
error: next.run.error || null,
|
|
305
277
|
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
306
278
|
})
|
|
307
279
|
next.resolve(result)
|
|
308
|
-
if (!failed && followup) {
|
|
309
|
-
scheduleMainLoopFollowup(next.run.sessionId, followup)
|
|
310
|
-
log.info('session-run', `Queued main-loop followup after ${next.run.id}`, {
|
|
311
|
-
sessionId: next.run.sessionId,
|
|
312
|
-
delayMs: followup.delayMs,
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
280
|
} catch (err: any) {
|
|
316
281
|
const aborted = next.signalController.signal.aborted
|
|
317
282
|
next.run.status = aborted ? 'cancelled' : 'failed'
|
|
@@ -324,19 +289,6 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
324
289
|
error: next.run.error,
|
|
325
290
|
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
326
291
|
})
|
|
327
|
-
try {
|
|
328
|
-
handleMainLoopRunResult({
|
|
329
|
-
sessionId: next.run.sessionId,
|
|
330
|
-
message: next.message,
|
|
331
|
-
internal: next.run.internal,
|
|
332
|
-
source: next.run.source,
|
|
333
|
-
resultText: '',
|
|
334
|
-
error: next.run.error,
|
|
335
|
-
toolEvents: [],
|
|
336
|
-
})
|
|
337
|
-
} catch {
|
|
338
|
-
// Main-loop bookkeeping failures should not affect queue execution.
|
|
339
|
-
}
|
|
340
292
|
next.reject(err instanceof Error ? err : new Error(next.run.error))
|
|
341
293
|
} finally {
|
|
342
294
|
if (runtimeTimer) clearTimeout(runtimeTimer)
|
|
@@ -528,7 +480,9 @@ export function getSessionRunState(sessionId: string): {
|
|
|
528
480
|
const running = state.runningByExecution.get(executionKey)
|
|
529
481
|
const queued = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId).length
|
|
530
482
|
return {
|
|
531
|
-
runningRunId: running?.run.sessionId === sessionId
|
|
483
|
+
runningRunId: (running?.run.sessionId === sessionId && running.run.status === 'running')
|
|
484
|
+
? running.run.id
|
|
485
|
+
: undefined,
|
|
532
486
|
queueLength: queued,
|
|
533
487
|
}
|
|
534
488
|
}
|
|
@@ -562,8 +516,7 @@ export function cancelSessionRuns(sessionId: string, reason = 'Cancelled'): { ca
|
|
|
562
516
|
let cancelledRunning = false
|
|
563
517
|
if (running && running.run.sessionId === sessionId) {
|
|
564
518
|
cancelledRunning = true
|
|
565
|
-
running
|
|
566
|
-
try { active.get(sessionId)?.kill?.() } catch { /* noop */ }
|
|
519
|
+
abortSessionRuntime(running, reason)
|
|
567
520
|
}
|
|
568
521
|
const cancelledQueued = cancelPendingForSession(sessionId, reason)
|
|
569
522
|
return { cancelledQueued, cancelledRunning }
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { describe, it } from 'node:test'
|
|
5
|
+
|
|
6
|
+
const thisFile = new URL(import.meta.url).pathname
|
|
7
|
+
const toolsDir = path.dirname(thisFile)
|
|
8
|
+
const serverDir = path.resolve(toolsDir, '..')
|
|
9
|
+
|
|
10
|
+
function readToolSource(fileName: string): string {
|
|
11
|
+
return fs.readFileSync(path.join(toolsDir, fileName), 'utf-8')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readServerSource(fileName: string): string {
|
|
15
|
+
return fs.readFileSync(path.join(serverDir, fileName), 'utf-8')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('browser workflow surface', () => {
|
|
19
|
+
it('advertises the higher-level browser actions in web.ts', () => {
|
|
20
|
+
const src = readToolSource('web.ts')
|
|
21
|
+
for (const action of ['read_page', 'extract_links', 'extract_form_fields', 'extract_table', 'fill_form', 'submit_form', 'scroll_until', 'download_file', 'complete_web_task']) {
|
|
22
|
+
assert.equal(src.includes(`'${action}'`), true, `web.ts should expose ${action}`)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('durable wait surface', () => {
|
|
28
|
+
it('advertises the durable wait actions in monitor.ts', () => {
|
|
29
|
+
const src = readToolSource('monitor.ts')
|
|
30
|
+
for (const action of ['wait_until', 'wait_for_http', 'wait_for_file', 'wait_for_task', 'wait_for_webhook', 'wait_for_page_change']) {
|
|
31
|
+
assert.equal(src.includes(`'${action}'`), true, `monitor.ts should expose ${action}`)
|
|
32
|
+
}
|
|
33
|
+
assert.equal(src.includes('createDurableWatch'), true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('routes schedule_wake through durable watch storage', () => {
|
|
37
|
+
const src = readToolSource('schedule.ts')
|
|
38
|
+
assert.equal(src.includes('createWatchJob'), true)
|
|
39
|
+
assert.equal(src.includes("type: 'time'"), true)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('delegation job handles', () => {
|
|
44
|
+
it('exposes subagent control actions', () => {
|
|
45
|
+
const src = readToolSource('subagent.ts')
|
|
46
|
+
for (const action of ['status', 'list', 'wait', 'cancel']) {
|
|
47
|
+
assert.equal(src.includes(`action === '${action}'`), true, `subagent.ts should handle ${action}`)
|
|
48
|
+
}
|
|
49
|
+
assert.equal(src.includes('createDelegationJob'), true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('builds delegate context from the invoking session and uses job records', () => {
|
|
53
|
+
const src = readToolSource('delegate.ts')
|
|
54
|
+
assert.equal(src.includes('buildDelegateContextFromSessionish'), true)
|
|
55
|
+
assert.equal(src.includes('createDelegationJob'), true)
|
|
56
|
+
assert.equal(src.includes('waitForDelegateJob'), true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('scheduler and daemon recover the durable autonomy jobs', () => {
|
|
60
|
+
const schedulerSrc = readServerSource('scheduler.ts')
|
|
61
|
+
const daemonSrc = readServerSource('daemon-state.ts')
|
|
62
|
+
assert.equal(schedulerSrc.includes('processDueWatchJobs'), true)
|
|
63
|
+
assert.equal(daemonSrc.includes('recoverStaleDelegationJobs'), true)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('primitive plugin surfaces', () => {
|
|
68
|
+
it('advertises mailbox and human-loop actions', () => {
|
|
69
|
+
const mailboxSrc = readToolSource('mailbox.ts')
|
|
70
|
+
const humanSrc = readToolSource('human-loop.ts')
|
|
71
|
+
for (const action of ['list_messages', 'list_threads', 'search_messages', 'read_message', 'download_attachment', 'reply', 'wait_for_email']) {
|
|
72
|
+
assert.equal(mailboxSrc.includes(`'${action}'`), true, `mailbox.ts should expose ${action}`)
|
|
73
|
+
}
|
|
74
|
+
for (const action of ['request_input', 'request_approval', 'wait_for_reply', 'wait_for_approval', 'list_mailbox', 'ack_mailbox', 'status']) {
|
|
75
|
+
assert.equal(humanSrc.includes(`'${action}'`), true, `human-loop.ts should expose ${action}`)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('advertises document, extract, table, and crawl actions', () => {
|
|
80
|
+
const documentSrc = readToolSource('document.ts')
|
|
81
|
+
const extractSrc = readToolSource('extract.ts')
|
|
82
|
+
const tableSrc = readToolSource('table.ts')
|
|
83
|
+
const crawlSrc = readToolSource('crawl.ts')
|
|
84
|
+
|
|
85
|
+
for (const action of ['read', 'metadata', 'ocr', 'extract_tables', 'store', 'list', 'search', 'get', 'delete']) {
|
|
86
|
+
assert.equal(documentSrc.includes(`'${action}'`), true, `document.ts should expose ${action}`)
|
|
87
|
+
}
|
|
88
|
+
for (const action of ['extract_structured', 'summarize', 'status']) {
|
|
89
|
+
assert.equal(extractSrc.includes(`'${action}'`), true, `extract.ts should expose ${action}`)
|
|
90
|
+
}
|
|
91
|
+
for (const action of ['read', 'load_csv', 'load_xlsx', 'summarize', 'filter', 'sort', 'group', 'pivot', 'dedupe', 'join', 'write']) {
|
|
92
|
+
assert.equal(tableSrc.includes(`'${action}'`), true, `table.ts should expose ${action}`)
|
|
93
|
+
}
|
|
94
|
+
for (const action of ['crawl_site', 'follow_pagination', 'extract_sitemap', 'dedupe_pages', 'batch_extract']) {
|
|
95
|
+
assert.equal(crawlSrc.includes(`'${action}'`), true, `crawl.ts should expose ${action}`)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('registers the primitive plugins in builtin-plugins.ts', () => {
|
|
100
|
+
const src = readServerSource('builtin-plugins.ts')
|
|
101
|
+
for (const moduleName of ['mailbox', 'human-loop', 'document', 'extract', 'table', 'crawl']) {
|
|
102
|
+
assert.equal(src.includes(`session-tools/${moduleName}`), true, `builtin-plugins.ts should import ${moduleName}`)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -3,7 +3,6 @@ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
|
3
3
|
import type { Plugin, PluginHooks } from '@/types'
|
|
4
4
|
import { getPluginManager } from '../plugins'
|
|
5
5
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
6
|
-
import { loadSettings } from '../storage'
|
|
7
6
|
import type { ToolBuildContext } from './context'
|
|
8
7
|
|
|
9
8
|
type CalendarProvider = 'google' | 'outlook'
|
|
@@ -18,8 +17,7 @@ interface CalendarConfig {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
function getConfig(): CalendarConfig {
|
|
21
|
-
const
|
|
22
|
-
const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.calendar ?? {}
|
|
20
|
+
const ps = getPluginManager().getPluginSettings('calendar')
|
|
23
21
|
return {
|
|
24
22
|
provider: (ps.provider as CalendarProvider) || 'google',
|
|
25
23
|
accessToken: (ps.accessToken as string) || '',
|
|
@@ -49,15 +47,7 @@ async function refreshGoogleToken(cfg: CalendarConfig): Promise<string | null> {
|
|
|
49
47
|
const data = await res.json()
|
|
50
48
|
const newToken = data?.access_token as string | undefined
|
|
51
49
|
if (newToken) {
|
|
52
|
-
|
|
53
|
-
const settings = loadSettings()
|
|
54
|
-
const pluginSettings = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined) ?? {}
|
|
55
|
-
const calSettings = pluginSettings.calendar ?? {}
|
|
56
|
-
calSettings.accessToken = newToken
|
|
57
|
-
pluginSettings.calendar = calSettings
|
|
58
|
-
settings.pluginSettings = pluginSettings
|
|
59
|
-
const { saveSettings } = await import('../storage')
|
|
60
|
-
saveSettings(settings)
|
|
50
|
+
getPluginManager().setPluginSettings('calendar', { ...cfg, accessToken: newToken })
|
|
61
51
|
}
|
|
62
52
|
return newToken || null
|
|
63
53
|
} catch {
|