typeclaw 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -13
- package/auth.schema.json +41 -0
- package/cron.schema.json +8 -0
- package/package.json +1 -1
- package/secrets.schema.json +41 -0
- package/src/agent/auth.ts +45 -22
- package/src/agent/index.ts +189 -19
- package/src/agent/multimodal/index.ts +12 -0
- package/src/agent/multimodal/look-at.ts +185 -0
- package/src/agent/multimodal/looker.ts +145 -0
- package/src/agent/plugin-tools.ts +30 -1
- package/src/agent/session-origin.ts +194 -46
- package/src/agent/subagents.ts +57 -1
- package/src/agent/system-prompt.ts +1 -1
- package/src/agent/tool-result-budget.ts +121 -0
- package/src/bundled-plugins/backup/index.ts +23 -8
- package/src/bundled-plugins/backup/runner.ts +22 -0
- package/src/bundled-plugins/memory/README.md +13 -10
- package/src/bundled-plugins/memory/append-tool.ts +87 -61
- package/src/bundled-plugins/memory/dreaming.ts +137 -7
- package/src/bundled-plugins/memory/find-entry-tool.ts +62 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +19 -44
- package/src/bundled-plugins/memory/index.ts +91 -8
- package/src/bundled-plugins/memory/load-memory.ts +74 -34
- package/src/bundled-plugins/memory/memory-logger.ts +72 -29
- package/src/bundled-plugins/memory/migration.ts +276 -0
- package/src/bundled-plugins/memory/stream-events.ts +55 -0
- package/src/bundled-plugins/memory/stream-io.ts +63 -0
- package/src/bundled-plugins/memory/watermark.ts +48 -8
- package/src/bundled-plugins/security/index.ts +103 -10
- package/src/bundled-plugins/security/permissions.ts +12 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +51 -18
- package/src/bundled-plugins/tool-result-cap/README.md +9 -4
- package/src/bundled-plugins/tool-result-cap/cap-jsonl.ts +115 -0
- package/src/bundled-plugins/tool-result-cap/cap-result.ts +25 -13
- package/src/bundled-plugins/tool-result-cap/index.ts +16 -2
- package/src/channels/adapters/discord-bot-classify.ts +2 -6
- package/src/channels/adapters/discord-bot.ts +4 -45
- package/src/channels/adapters/kakaotalk-classify.ts +3 -7
- package/src/channels/adapters/kakaotalk.ts +28 -47
- package/src/channels/adapters/slack-bot-classify.ts +2 -6
- package/src/channels/adapters/slack-bot.ts +4 -50
- package/src/channels/adapters/telegram-bot-classify.ts +8 -10
- package/src/channels/adapters/telegram-bot.ts +3 -16
- package/src/channels/index.ts +3 -2
- package/src/channels/manager.ts +15 -1
- package/src/channels/persistence.ts +44 -10
- package/src/channels/router.ts +228 -19
- package/src/channels/schema.ts +6 -156
- package/src/cli/channel.ts +200 -4
- package/src/cli/compose-usage.ts +182 -0
- package/src/cli/compose.ts +33 -0
- package/src/cli/hostd.ts +49 -1
- package/src/cli/index.ts +4 -0
- package/src/cli/init.ts +809 -300
- package/src/cli/model.ts +244 -0
- package/src/cli/provider.ts +404 -0
- package/src/cli/reload.ts +11 -3
- package/src/cli/role.ts +156 -0
- package/src/cli/run.ts +3 -1
- package/src/cli/tui.ts +13 -3
- package/src/cli/usage-args.ts +47 -0
- package/src/cli/usage.ts +97 -0
- package/src/compose/index.ts +1 -0
- package/src/compose/usage.ts +65 -0
- package/src/config/config.ts +491 -19
- package/src/config/index.ts +15 -1
- package/src/config/models-mutation.ts +200 -0
- package/src/config/providers-mutation.ts +250 -0
- package/src/config/providers.ts +141 -2
- package/src/config/reloadable.ts +15 -4
- package/src/container/index.ts +6 -1
- package/src/container/port.ts +10 -0
- package/src/container/require-running.ts +33 -0
- package/src/container/start.ts +81 -63
- package/src/cron/consumer.ts +22 -2
- package/src/cron/index.ts +45 -4
- package/src/cron/schema.ts +104 -0
- package/src/doctor/checks.ts +51 -34
- package/src/doctor/plugin-bridge.ts +28 -4
- package/src/git/system-commit.ts +103 -0
- package/src/hostd/daemon.ts +16 -0
- package/src/hostd/kakao-renewal-manager.ts +223 -0
- package/src/hostd/paths.ts +7 -0
- package/src/init/dockerfile.ts +36 -10
- package/src/init/gitignore.ts +1 -1
- package/src/init/index.ts +213 -85
- package/src/init/kakaotalk-auth.ts +18 -1
- package/src/init/models-dev.ts +26 -1
- package/src/init/run-owner-claim.ts +77 -0
- package/src/permissions/builtins.ts +70 -0
- package/src/permissions/grant.ts +99 -0
- package/src/permissions/index.ts +29 -0
- package/src/permissions/match-rule.ts +305 -0
- package/src/permissions/permissions.ts +196 -0
- package/src/permissions/resolve.ts +80 -0
- package/src/permissions/schema.ts +79 -0
- package/src/plugin/context.ts +8 -4
- package/src/plugin/define.ts +2 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/manager.ts +41 -0
- package/src/plugin/registry.ts +9 -0
- package/src/plugin/types.ts +35 -1
- package/src/reload/client.ts +25 -1
- package/src/role-claim/client.ts +182 -0
- package/src/role-claim/code.ts +53 -0
- package/src/role-claim/controller.ts +194 -0
- package/src/role-claim/index.ts +19 -0
- package/src/role-claim/match-rule.ts +43 -0
- package/src/role-claim/pending.ts +100 -0
- package/src/run/channel-session-factory.ts +76 -5
- package/src/run/index.ts +68 -7
- package/src/secrets/encryption.ts +116 -0
- package/src/secrets/kakao-renewal.ts +248 -0
- package/src/secrets/kakao-store.ts +66 -7
- package/src/secrets/keys.ts +173 -0
- package/src/secrets/schema.ts +23 -0
- package/src/secrets/storage.ts +83 -0
- package/src/server/index.ts +198 -71
- package/src/shared/index.ts +4 -0
- package/src/shared/protocol.ts +27 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +3 -3
- package/src/skills/typeclaw-config/SKILL.md +104 -112
- package/src/skills/typeclaw-memory/SKILL.md +9 -9
- package/src/skills/typeclaw-permissions/SKILL.md +166 -0
- package/src/stream/types.ts +7 -1
- package/src/tui/client.ts +66 -5
- package/src/tui/index.ts +61 -9
- package/src/usage/aggregate.ts +117 -0
- package/src/usage/format.ts +30 -0
- package/src/usage/index.ts +68 -0
- package/src/usage/report.ts +354 -0
- package/src/usage/scan.ts +186 -0
- package/typeclaw.schema.json +134 -98
package/src/server/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ChannelRouter } from '@/channels/router'
|
|
|
12
12
|
import type { HookBus } from '@/plugin'
|
|
13
13
|
import type { BrokerWsData, ContainerBroker } from '@/portbroker'
|
|
14
14
|
import type { ReloadAllResult, ReloadRegistry } from '@/reload'
|
|
15
|
+
import type { ClaimController, ClaimResultEvent } from '@/role-claim'
|
|
15
16
|
import type { PluginRuntime, PluginRuntimeState } from '@/run/plugin-runtime'
|
|
16
17
|
import type { SessionFactory } from '@/sessions'
|
|
17
18
|
import type { ClientMessage, PromptDelivery, QueueStateItem, ReloadResultPayload, ServerMessage } from '@/shared'
|
|
@@ -20,6 +21,12 @@ import type { Stream, StreamMessage, StreamMessageId, Unsubscribe } from '@/stre
|
|
|
20
21
|
export type ReloadAllFn = () => Promise<ReloadAllResult>
|
|
21
22
|
export type CreateSessionFn = (options?: CreateSessionOptions) => Promise<AgentSession | CreateSessionResult>
|
|
22
23
|
|
|
24
|
+
export type ServerLogger = {
|
|
25
|
+
info: (msg: string) => void
|
|
26
|
+
warn: (msg: string) => void
|
|
27
|
+
error: (msg: string) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export type ServerOptions = {
|
|
24
31
|
port: number
|
|
25
32
|
reloadAll?: ReloadAllFn
|
|
@@ -31,11 +38,28 @@ export type ServerOptions = {
|
|
|
31
38
|
agentDir?: string
|
|
32
39
|
pluginRuntime?: PluginRuntime
|
|
33
40
|
containerName?: string
|
|
41
|
+
tuiToken?: string
|
|
34
42
|
// Optional in-process portbroker handler. When provided, requests to the
|
|
35
43
|
// /portbroker WS path are routed to it instead of being treated as TUI
|
|
36
44
|
// sessions. Omit to keep TUI-only behavior (used by tests + non-container
|
|
37
45
|
// dev runs).
|
|
38
46
|
containerBroker?: ContainerBroker
|
|
47
|
+
// Optional logger for server-side events. Defaults to `consoleLogger`
|
|
48
|
+
// which writes to stdout/stderr so `typeclaw logs` surfaces every event.
|
|
49
|
+
// Tests inject a fake logger to assert on captured output.
|
|
50
|
+
logger?: ServerLogger
|
|
51
|
+
// Optional role-claim controller. When set, the server accepts
|
|
52
|
+
// `claim_start` / `claim_cancel` from TUI-class WS clients (the host
|
|
53
|
+
// CLI's `typeclaw role claim` command in particular), and pushes
|
|
54
|
+
// `claim_started` / `claim_completed` / `claim_error` back over the
|
|
55
|
+
// same connection. Omitted in tests that don't exercise the flow.
|
|
56
|
+
claimController?: ClaimController
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const consoleLogger: ServerLogger = {
|
|
60
|
+
info: (m) => console.log(m),
|
|
61
|
+
warn: (m) => console.warn(m),
|
|
62
|
+
error: (m) => console.error(m),
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
export type Server = ReturnType<typeof createServer>
|
|
@@ -60,6 +84,8 @@ type SessionState = {
|
|
|
60
84
|
draining: boolean
|
|
61
85
|
unsubBroadcast: Unsubscribe | null
|
|
62
86
|
unsubPrompts: Unsubscribe | null
|
|
87
|
+
unsubClaim: Unsubscribe | null
|
|
88
|
+
activeClaimCode: string | null
|
|
63
89
|
// Captured at session open so close-time hooks fire against the same
|
|
64
90
|
// generation that ran session.start. A plugin reload mid-connection does
|
|
65
91
|
// not re-target this session's lifecycle hooks.
|
|
@@ -82,7 +108,10 @@ export function createServer({
|
|
|
82
108
|
agentDir,
|
|
83
109
|
pluginRuntime,
|
|
84
110
|
containerName,
|
|
111
|
+
tuiToken,
|
|
85
112
|
containerBroker,
|
|
113
|
+
logger = consoleLogger,
|
|
114
|
+
claimController,
|
|
86
115
|
}: ServerOptions) {
|
|
87
116
|
const sessionStates = new WeakMap<Ws, SessionState>()
|
|
88
117
|
|
|
@@ -97,6 +126,9 @@ export function createServer({
|
|
|
97
126
|
if (server.upgrade(req, { data })) return
|
|
98
127
|
return new Response('upgrade failed', { status: 400 })
|
|
99
128
|
}
|
|
129
|
+
if (isWebSocketUpgrade(req) && tuiToken !== undefined && url.searchParams.get('token') !== tuiToken) {
|
|
130
|
+
return new Response('unauthorized', { status: 401 })
|
|
131
|
+
}
|
|
100
132
|
const sessionId = crypto.randomUUID()
|
|
101
133
|
const data: TuiWsData = { kind: 'tui', sessionId }
|
|
102
134
|
if (server.upgrade(req, { data })) return
|
|
@@ -109,73 +141,82 @@ export function createServer({
|
|
|
109
141
|
return
|
|
110
142
|
}
|
|
111
143
|
const ws = rawWs as Ws
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
144
|
+
try {
|
|
145
|
+
const sessionManager = sessionFactory?.createPersisted()
|
|
146
|
+
const sessionFileId = sessionManager?.getSessionId() ?? ws.data.sessionId
|
|
147
|
+
// Snapshot the runtime once so the entire session lifecycle for this
|
|
148
|
+
// ws connection sees one consistent generation of registry+hooks. A
|
|
149
|
+
// reload landing mid-connection swaps the live pointer; this session
|
|
150
|
+
// keeps using the snapshot it was created with until close.
|
|
151
|
+
const runtimeSnapshot = pluginRuntime?.get()
|
|
152
|
+
const pluginsWiring =
|
|
153
|
+
runtimeSnapshot !== undefined && agentDir !== undefined
|
|
154
|
+
? {
|
|
155
|
+
registry: runtimeSnapshot.registry,
|
|
156
|
+
hooks: runtimeSnapshot.hooks,
|
|
157
|
+
sessionId: sessionFileId,
|
|
158
|
+
agentDir,
|
|
159
|
+
}
|
|
160
|
+
: undefined
|
|
161
|
+
const origin: SessionOrigin = { kind: 'tui', sessionId: sessionFileId }
|
|
162
|
+
const result = await createSession({
|
|
163
|
+
reloadRegistry,
|
|
164
|
+
sessionManager,
|
|
165
|
+
origin,
|
|
166
|
+
...(stream ? { stream } : {}),
|
|
167
|
+
...(channelRouter ? { channelRouter } : {}),
|
|
168
|
+
...(pluginsWiring ? { plugins: pluginsWiring } : {}),
|
|
169
|
+
...(containerName !== undefined ? { containerName } : {}),
|
|
170
|
+
})
|
|
171
|
+
const session = 'session' in result ? result.session : result
|
|
172
|
+
const dispose = 'session' in result && result.dispose ? result.dispose : async () => {}
|
|
173
|
+
|
|
174
|
+
const state: SessionState = {
|
|
175
|
+
session,
|
|
176
|
+
sessionFileId,
|
|
177
|
+
origin,
|
|
178
|
+
sessionManager,
|
|
179
|
+
drainQueue: [],
|
|
180
|
+
draining: false,
|
|
181
|
+
unsubBroadcast: null,
|
|
182
|
+
unsubPrompts: null,
|
|
183
|
+
unsubClaim: null,
|
|
184
|
+
activeClaimCode: null,
|
|
185
|
+
runtimeSnapshot: runtimeSnapshot ?? null,
|
|
186
|
+
dispose,
|
|
187
|
+
}
|
|
188
|
+
sessionStates.set(ws, state)
|
|
154
189
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
190
|
+
if (runtimeSnapshot !== undefined && agentDir !== undefined) {
|
|
191
|
+
await runtimeSnapshot.hooks.runSessionStart({ sessionId: sessionFileId, agentDir })
|
|
192
|
+
}
|
|
158
193
|
|
|
159
|
-
|
|
194
|
+
forwardSessionEvents(ws, session, logger, sessionFileId)
|
|
160
195
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
if (stream) {
|
|
197
|
+
state.unsubPrompts = stream.subscribe({ target: { kind: 'session', sessionId: sessionFileId } }, (msg) =>
|
|
198
|
+
enqueuePrompt(ws, state, msg, agentDir, logger),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
state.unsubBroadcast = stream.subscribe({ target: { kind: 'broadcast' } }, (msg) => {
|
|
202
|
+
const payload: ServerMessage = {
|
|
203
|
+
type: 'notification',
|
|
204
|
+
payload: msg.payload,
|
|
205
|
+
...(msg.replyTo !== undefined ? { replyTo: msg.replyTo } : {}),
|
|
206
|
+
...(msg.meta !== undefined ? { meta: msg.meta } : {}),
|
|
207
|
+
}
|
|
208
|
+
send(ws, payload)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
165
211
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
send(ws, payload)
|
|
174
|
-
})
|
|
212
|
+
send(ws, { type: 'connected', sessionId: sessionFileId })
|
|
213
|
+
console.log(`session ${sessionFileId}: open`)
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
216
|
+
console.error(`session ${ws.data.sessionId}: open failed: ${message}`)
|
|
217
|
+
send(ws, { type: 'error', message })
|
|
218
|
+
ws.close()
|
|
175
219
|
}
|
|
176
|
-
|
|
177
|
-
send(ws, { type: 'connected', sessionId: sessionFileId })
|
|
178
|
-
console.log(`session ${sessionFileId}: open`)
|
|
179
220
|
},
|
|
180
221
|
async message(rawWs, raw) {
|
|
181
222
|
if (rawWs.data.kind === 'portbroker') {
|
|
@@ -186,6 +227,73 @@ export function createServer({
|
|
|
186
227
|
const msg = JSON.parse(String(raw)) as ClientMessage
|
|
187
228
|
const state = sessionStates.get(ws)
|
|
188
229
|
|
|
230
|
+
if (msg.type === 'claim_start') {
|
|
231
|
+
if (!state) return
|
|
232
|
+
if (!claimController) {
|
|
233
|
+
send(ws, {
|
|
234
|
+
type: 'claim_error',
|
|
235
|
+
payload: { code: msg.code, reason: 'role-claim is not enabled on this agent' },
|
|
236
|
+
})
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (state.unsubClaim) {
|
|
240
|
+
state.unsubClaim()
|
|
241
|
+
state.unsubClaim = null
|
|
242
|
+
}
|
|
243
|
+
const result = claimController.startClaim({
|
|
244
|
+
code: msg.code,
|
|
245
|
+
role: msg.role,
|
|
246
|
+
ttlMs: msg.ttlMs,
|
|
247
|
+
...(msg.channel !== undefined ? { channel: msg.channel } : {}),
|
|
248
|
+
})
|
|
249
|
+
if (!result.ok) {
|
|
250
|
+
send(ws, { type: 'claim_error', payload: { code: msg.code, reason: result.reason } })
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
state.activeClaimCode = msg.code
|
|
254
|
+
state.unsubClaim = claimController.onResult((event: ClaimResultEvent) => {
|
|
255
|
+
if (event.kind === 'completed' && event.code === msg.code) {
|
|
256
|
+
send(ws, {
|
|
257
|
+
type: 'claim_completed',
|
|
258
|
+
payload: {
|
|
259
|
+
code: event.code,
|
|
260
|
+
role: event.role,
|
|
261
|
+
matchRule: event.matchRule,
|
|
262
|
+
adapter: event.adapter,
|
|
263
|
+
authorId: event.authorId,
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
} else if (event.kind === 'error' && event.code === msg.code) {
|
|
267
|
+
send(ws, { type: 'claim_error', payload: { code: event.code, reason: event.reason } })
|
|
268
|
+
} else if (event.kind === 'cancelled' && event.code === msg.code) {
|
|
269
|
+
send(ws, { type: 'claim_error', payload: { code: event.code, reason: 'cancelled' } })
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
send(ws, {
|
|
273
|
+
type: 'claim_started',
|
|
274
|
+
payload: {
|
|
275
|
+
code: msg.code,
|
|
276
|
+
role: msg.role,
|
|
277
|
+
...(msg.channel !== undefined ? { channel: msg.channel } : {}),
|
|
278
|
+
expiresAt: result.expiresAt,
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (msg.type === 'claim_cancel') {
|
|
285
|
+
if (!state || !claimController) return
|
|
286
|
+
if (state.activeClaimCode !== null) {
|
|
287
|
+
claimController.cancelClaim(state.activeClaimCode)
|
|
288
|
+
state.activeClaimCode = null
|
|
289
|
+
}
|
|
290
|
+
if (state.unsubClaim) {
|
|
291
|
+
state.unsubClaim()
|
|
292
|
+
state.unsubClaim = null
|
|
293
|
+
}
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
189
297
|
if (msg.type === 'reload') {
|
|
190
298
|
await handleReload(ws, reloadAll, reloadRegistry, msg.scope)
|
|
191
299
|
return
|
|
@@ -238,7 +346,9 @@ export function createServer({
|
|
|
238
346
|
await state.session.prompt(msg.text)
|
|
239
347
|
send(ws, { type: 'done' })
|
|
240
348
|
} catch (err) {
|
|
241
|
-
|
|
349
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
350
|
+
logger.error(`[server] ${state.sessionFileId}: prompt failed: ${message}`)
|
|
351
|
+
send(ws, { type: 'error', message })
|
|
242
352
|
}
|
|
243
353
|
if (fallbackHooks !== undefined && agentDir !== undefined) {
|
|
244
354
|
await fallbackHooks.runSessionTurnEnd({
|
|
@@ -266,9 +376,13 @@ export function createServer({
|
|
|
266
376
|
const state = sessionStates.get(ws)
|
|
267
377
|
state?.unsubBroadcast?.()
|
|
268
378
|
state?.unsubPrompts?.()
|
|
379
|
+
state?.unsubClaim?.()
|
|
380
|
+
if (state?.activeClaimCode !== null && state?.activeClaimCode !== undefined && claimController) {
|
|
381
|
+
claimController.cancelClaim(state.activeClaimCode)
|
|
382
|
+
}
|
|
269
383
|
try {
|
|
270
384
|
if (state && state.runtimeSnapshot !== null) {
|
|
271
|
-
await state.runtimeSnapshot.hooks.runSessionEnd({ sessionId: state.sessionFileId })
|
|
385
|
+
await state.runtimeSnapshot.hooks.runSessionEnd({ sessionId: state.sessionFileId, origin: state.origin })
|
|
272
386
|
}
|
|
273
387
|
} finally {
|
|
274
388
|
if (state) {
|
|
@@ -289,7 +403,11 @@ export function createServer({
|
|
|
289
403
|
return { start }
|
|
290
404
|
}
|
|
291
405
|
|
|
292
|
-
function
|
|
406
|
+
function isWebSocketUpgrade(req: Request): boolean {
|
|
407
|
+
return req.headers.get('upgrade')?.toLowerCase() === 'websocket'
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function forwardSessionEvents(ws: Ws, session: AgentSession, logger: ServerLogger, sessionFileId: string): void {
|
|
293
411
|
const toolStartedAt = new Map<string, number>()
|
|
294
412
|
|
|
295
413
|
session.subscribe((event) => {
|
|
@@ -307,7 +425,7 @@ function forwardSessionEvents(ws: Ws, session: AgentSession): void {
|
|
|
307
425
|
// because no text deltas were ever emitted, which looks like a freeze.
|
|
308
426
|
// The server's existing try/catch around `session.prompt()` only
|
|
309
427
|
// catches throws, so it never sees these.
|
|
310
|
-
forwardAssistantError(ws, event.message)
|
|
428
|
+
forwardAssistantError(ws, event.message, logger, sessionFileId)
|
|
311
429
|
break
|
|
312
430
|
case 'tool_execution_start':
|
|
313
431
|
toolStartedAt.set(event.toolCallId, Date.now())
|
|
@@ -336,7 +454,7 @@ function forwardSessionEvents(ws: Ws, session: AgentSession): void {
|
|
|
336
454
|
})
|
|
337
455
|
}
|
|
338
456
|
|
|
339
|
-
function forwardAssistantError(ws: Ws, message: unknown): void {
|
|
457
|
+
function forwardAssistantError(ws: Ws, message: unknown, logger: ServerLogger, sessionFileId: string): void {
|
|
340
458
|
if (typeof message !== 'object' || message === null) return
|
|
341
459
|
const m = message as { role?: string; stopReason?: string; errorMessage?: string }
|
|
342
460
|
if (m.role !== 'assistant') return
|
|
@@ -345,10 +463,17 @@ function forwardAssistantError(ws: Ws, message: unknown): void {
|
|
|
345
463
|
// error message because the TUI already shows abort feedback elsewhere.
|
|
346
464
|
if (m.stopReason === 'aborted') return
|
|
347
465
|
const text = typeof m.errorMessage === 'string' && m.errorMessage.length > 0 ? m.errorMessage : 'LLM call failed'
|
|
466
|
+
logger.error(`[server] ${sessionFileId}: LLM call failed: ${text}`)
|
|
348
467
|
send(ws, { type: 'error', message: text })
|
|
349
468
|
}
|
|
350
469
|
|
|
351
|
-
function enqueuePrompt(
|
|
470
|
+
function enqueuePrompt(
|
|
471
|
+
ws: Ws,
|
|
472
|
+
state: SessionState,
|
|
473
|
+
msg: StreamMessage,
|
|
474
|
+
agentDir: string | undefined,
|
|
475
|
+
logger: ServerLogger,
|
|
476
|
+
): void {
|
|
352
477
|
const payload = msg.payload as { kind?: string; text?: string; delivery?: PromptDelivery }
|
|
353
478
|
if (payload?.kind !== 'prompt' || typeof payload.text !== 'string') return
|
|
354
479
|
const delivery: PromptDelivery = payload.delivery ?? 'queue'
|
|
@@ -364,7 +489,7 @@ function enqueuePrompt(ws: Ws, state: SessionState, msg: StreamMessage, agentDir
|
|
|
364
489
|
ts: msg.ts,
|
|
365
490
|
})
|
|
366
491
|
pushQueueState(ws, state)
|
|
367
|
-
void drain(ws, state, agentDir)
|
|
492
|
+
void drain(ws, state, agentDir, logger)
|
|
368
493
|
}
|
|
369
494
|
|
|
370
495
|
// `session.idle` semantically means "the agent finished a prompt and is now
|
|
@@ -400,7 +525,7 @@ function makeTurnHookCallers(
|
|
|
400
525
|
}
|
|
401
526
|
}
|
|
402
527
|
|
|
403
|
-
async function drain(ws: Ws, state: SessionState, agentDir: string | undefined): Promise<void> {
|
|
528
|
+
async function drain(ws: Ws, state: SessionState, agentDir: string | undefined, logger: ServerLogger): Promise<void> {
|
|
404
529
|
if (state.draining) return
|
|
405
530
|
state.draining = true
|
|
406
531
|
const fireIdle = makeIdleHookCaller(state)
|
|
@@ -417,7 +542,9 @@ async function drain(ws: Ws, state: SessionState, agentDir: string | undefined):
|
|
|
417
542
|
await state.session.prompt(item.text)
|
|
418
543
|
send(ws, { type: 'done' })
|
|
419
544
|
} catch (err) {
|
|
420
|
-
|
|
545
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
546
|
+
logger.error(`[server] ${state.sessionFileId}: prompt failed: ${message}`)
|
|
547
|
+
send(ws, { type: 'error', message })
|
|
421
548
|
}
|
|
422
549
|
await fireTurnEnd()
|
|
423
550
|
await fireIdle()
|
package/src/shared/index.ts
CHANGED
package/src/shared/protocol.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type DoctorFixPayload =
|
|
|
22
22
|
| { ok: true; checkId: string; summary: string; changedPaths: string[] }
|
|
23
23
|
| { ok: false; checkId: string; error: string }
|
|
24
24
|
|
|
25
|
+
export type ClaimRoleChoice = 'owner' | 'member' | 'trusted' | (string & {})
|
|
26
|
+
|
|
25
27
|
export type ClientMessage =
|
|
26
28
|
| { type: 'prompt'; text: string; delivery?: PromptDelivery }
|
|
27
29
|
| { type: 'reload'; scope?: string }
|
|
@@ -29,9 +31,31 @@ export type ClientMessage =
|
|
|
29
31
|
| { type: 'queue_cancel'; messageId: string }
|
|
30
32
|
| { type: 'doctor'; requestId: DoctorRequestId }
|
|
31
33
|
| { type: 'doctor_fix'; requestId: DoctorRequestId; checkId: string }
|
|
34
|
+
| { type: 'claim_start'; code: string; role: ClaimRoleChoice; channel?: string; ttlMs: number }
|
|
35
|
+
| { type: 'claim_cancel' }
|
|
32
36
|
|
|
33
37
|
export type QueueStateItem = { id: string; text: string; ts: number }
|
|
34
38
|
|
|
39
|
+
export type ClaimStartedPayload = {
|
|
40
|
+
code: string
|
|
41
|
+
role: string
|
|
42
|
+
channel?: string
|
|
43
|
+
expiresAt: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ClaimCompletedPayload = {
|
|
47
|
+
code: string
|
|
48
|
+
role: string
|
|
49
|
+
matchRule: string
|
|
50
|
+
adapter: string
|
|
51
|
+
authorId: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ClaimErrorPayload = {
|
|
55
|
+
code: string
|
|
56
|
+
reason: string
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
export type ServerMessage =
|
|
36
60
|
| { type: 'connected'; sessionId: string }
|
|
37
61
|
| { type: 'text_delta'; delta: string }
|
|
@@ -45,3 +69,6 @@ export type ServerMessage =
|
|
|
45
69
|
| { type: 'prompt_started'; messageId: string; text: string }
|
|
46
70
|
| { type: 'doctor_result'; requestId: DoctorRequestId; checks: DoctorCheckPayload[] }
|
|
47
71
|
| { type: 'doctor_fix_result'; requestId: DoctorRequestId; result: DoctorFixPayload }
|
|
72
|
+
| { type: 'claim_started'; payload: ClaimStartedPayload }
|
|
73
|
+
| { type: 'claim_completed'; payload: ClaimCompletedPayload }
|
|
74
|
+
| { type: 'claim_error'; payload: ClaimErrorPayload }
|
|
@@ -74,7 +74,7 @@ The adapter exposes three engagement triggers via `channels.kakaotalk.engagement
|
|
|
74
74
|
|
|
75
75
|
Stickiness behaves the same as Slack/Discord: once you've engaged in a chat, follow-up messages within `engagement.stickiness.perReply.window` ms will route to you regardless of trigger.
|
|
76
76
|
|
|
77
|
-
If you find yourself NOT receiving messages you expect to, the most likely cause is
|
|
77
|
+
If you find yourself NOT receiving messages you expect to, the most likely cause is missing role coverage in `roles.<role>.match[]`. KakaoTalk's match-rule grammar:
|
|
78
78
|
|
|
79
79
|
- `kakao:*` — every chat the account can see (use sparingly: this is every group and DM you are a member of)
|
|
80
80
|
- `kakao:dm/*` — every 1:1 chat
|
|
@@ -82,7 +82,7 @@ If you find yourself NOT receiving messages you expect to, the most likely cause
|
|
|
82
82
|
- `kakao:open/*` — every open chat
|
|
83
83
|
- `kakao:<chat-id>` — a specific chat by numeric chat_id
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Group chats with personal accounts are sensitive — every member sees every reply. Be conservative when widening match-rules: a `kakao:group/*` entry on a permissive role exposes the agent in every group the account is a member of. See the `typeclaw-permissions` skill.
|
|
86
86
|
|
|
87
87
|
## Mark read on every inbound
|
|
88
88
|
|
|
@@ -91,7 +91,7 @@ The adapter sends a LOCO `NOTIREAD` ack to KakaoTalk for every inbound message e
|
|
|
91
91
|
Things to know about this behavior:
|
|
92
92
|
|
|
93
93
|
- Auto-acking every received message is a distinct behavioral fingerprint compared to a human. A human reads messages when they open the chat; this adapter acks every received message instantly, even ones you never reply to. KakaoTalk's abuse detection may flag accounts that ack rapidly and unconditionally. **Run the kakaotalk adapter only on dedicated agent accounts you can afford to lose.**
|
|
94
|
-
- Dropped messages are still acked. If classify drops the message (your own self-sent loopback, empty text,
|
|
94
|
+
- Dropped messages are still acked. If classify drops the message (your own self-sent loopback, empty text, unknown chat), or the router drops it on the `channel.respond` gate, the unread "1" still clears — the agent has observed the bytes, so the read indicator should match.
|
|
95
95
|
- Open chats (오픈채팅) are skipped: the LOCO `NOTIREAD` packet needs a `linkId` for open chats and the adapter doesn't surface it yet. Unread counters in open chats will not decrement.
|
|
96
96
|
- The phone's home-screen OS unread badge may lag until the phone client foregrounds; the in-chat counter and other participants' indicators update immediately. KakaoTalk client quirk, not a typeclaw bug.
|
|
97
97
|
|