typeclaw 0.1.5 → 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 +14 -12
- 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 +7 -4
- package/src/bundled-plugins/memory/append-tool.ts +87 -61
- package/src/bundled-plugins/memory/dreaming.ts +23 -9
- 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 +799 -319
- package/src/cli/model.ts +244 -0
- package/src/cli/provider.ts +404 -0
- package/src/cli/reload.ts +6 -1
- package/src/cli/role.ts +156 -0
- package/src/cli/run.ts +3 -1
- package/src/cli/tui.ts +8 -1
- 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 +385 -12
- package/src/config/index.ts +7 -0
- 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 +5 -0
- package/src/container/require-running.ts +33 -0
- package/src/container/start.ts +39 -58
- 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 +50 -33
- 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 +32 -6
- package/src/init/index.ts +183 -62
- 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/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 +55 -6
- 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 +68 -0
- package/src/server/index.ts +122 -11
- 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 +38 -64
- package/src/skills/typeclaw-memory/SKILL.md +1 -1
- package/src/skills/typeclaw-permissions/SKILL.md +166 -0
- package/src/stream/types.ts +7 -1
- 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 +57 -45
package/src/secrets/storage.ts
CHANGED
|
@@ -175,6 +175,74 @@ export class SecretsBackend implements AuthStorageBackend {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// Returns a shallow snapshot of the `providers` slice. Used by post-init CLI
|
|
179
|
+
// commands (`typeclaw provider list/remove`, `typeclaw model list`) that need
|
|
180
|
+
// to inspect what's on disk without forcing AuthStorage's env-wins flatten —
|
|
181
|
+
// we want to show users which providers are file-backed vs env-overridden,
|
|
182
|
+
// and the flatten path collapses that distinction.
|
|
183
|
+
tryReadProvidersSync(): Providers {
|
|
184
|
+
if (!existsSync(this.secretsPath)) return {}
|
|
185
|
+
let release: (() => void) | undefined
|
|
186
|
+
try {
|
|
187
|
+
release = this.acquireSyncLockWithRetry()
|
|
188
|
+
return { ...this.readEnvelope().providers }
|
|
189
|
+
} finally {
|
|
190
|
+
release?.()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Atomic provider credential write. Idempotent at the type level: callers
|
|
195
|
+
// pass the full `ProviderCredential` (api_key or oauth), and the entry is
|
|
196
|
+
// merged into `providers.<id>` verbatim — same shape the schema accepts on
|
|
197
|
+
// read. Used by `typeclaw provider add/set` to write api-key credentials
|
|
198
|
+
// without going through AuthStorage's flatten/unflatten round-trip.
|
|
199
|
+
// OAuth flows MUST continue to go through `AuthStorage.login()` so refresh
|
|
200
|
+
// tokens land in the correct shape; this method is api-key oriented.
|
|
201
|
+
writeProviderCredentialSync(providerId: string, credential: ProviderCredential): void {
|
|
202
|
+
this.ensureParentDir()
|
|
203
|
+
this.ensureFileExists()
|
|
204
|
+
let release: (() => void) | undefined
|
|
205
|
+
try {
|
|
206
|
+
release = this.acquireSyncLockWithRetry()
|
|
207
|
+
const envelope = this.readEnvelope()
|
|
208
|
+
const next: SecretsFile = {
|
|
209
|
+
...envelope,
|
|
210
|
+
$schema: envelope.$schema ?? SCHEMA_REL,
|
|
211
|
+
version: SECRETS_FILE_VERSION,
|
|
212
|
+
providers: { ...envelope.providers, [providerId]: credential },
|
|
213
|
+
}
|
|
214
|
+
this.writeEnvelopeAtomic(next)
|
|
215
|
+
} finally {
|
|
216
|
+
release?.()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Removes `providers.<id>` from the envelope. Returns `true` when the
|
|
221
|
+
// provider was present and removed, `false` when nothing changed (idempotent
|
|
222
|
+
// on the CLI side — `provider remove fireworks` twice should not error on
|
|
223
|
+
// the second call). The file is rewritten only when something changed so
|
|
224
|
+
// canonical-shape reads pay zero cost.
|
|
225
|
+
removeProviderCredentialSync(providerId: string): boolean {
|
|
226
|
+
if (!existsSync(this.secretsPath)) return false
|
|
227
|
+
let release: (() => void) | undefined
|
|
228
|
+
try {
|
|
229
|
+
release = this.acquireSyncLockWithRetry()
|
|
230
|
+
const envelope = this.readEnvelope()
|
|
231
|
+
if (!(providerId in envelope.providers)) return false
|
|
232
|
+
const { [providerId]: _removed, ...rest } = envelope.providers
|
|
233
|
+
const next: SecretsFile = {
|
|
234
|
+
...envelope,
|
|
235
|
+
$schema: envelope.$schema ?? SCHEMA_REL,
|
|
236
|
+
version: SECRETS_FILE_VERSION,
|
|
237
|
+
providers: rest,
|
|
238
|
+
}
|
|
239
|
+
this.writeEnvelopeAtomic(next)
|
|
240
|
+
return true
|
|
241
|
+
} finally {
|
|
242
|
+
release?.()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
178
246
|
writeChannelsSync(next: Channels): void {
|
|
179
247
|
this.ensureParentDir()
|
|
180
248
|
this.ensureFileExists()
|
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
|
|
@@ -37,6 +44,22 @@ export type ServerOptions = {
|
|
|
37
44
|
// sessions. Omit to keep TUI-only behavior (used by tests + non-container
|
|
38
45
|
// dev runs).
|
|
39
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),
|
|
40
63
|
}
|
|
41
64
|
|
|
42
65
|
export type Server = ReturnType<typeof createServer>
|
|
@@ -61,6 +84,8 @@ type SessionState = {
|
|
|
61
84
|
draining: boolean
|
|
62
85
|
unsubBroadcast: Unsubscribe | null
|
|
63
86
|
unsubPrompts: Unsubscribe | null
|
|
87
|
+
unsubClaim: Unsubscribe | null
|
|
88
|
+
activeClaimCode: string | null
|
|
64
89
|
// Captured at session open so close-time hooks fire against the same
|
|
65
90
|
// generation that ran session.start. A plugin reload mid-connection does
|
|
66
91
|
// not re-target this session's lifecycle hooks.
|
|
@@ -85,6 +110,8 @@ export function createServer({
|
|
|
85
110
|
containerName,
|
|
86
111
|
tuiToken,
|
|
87
112
|
containerBroker,
|
|
113
|
+
logger = consoleLogger,
|
|
114
|
+
claimController,
|
|
88
115
|
}: ServerOptions) {
|
|
89
116
|
const sessionStates = new WeakMap<Ws, SessionState>()
|
|
90
117
|
|
|
@@ -153,6 +180,8 @@ export function createServer({
|
|
|
153
180
|
draining: false,
|
|
154
181
|
unsubBroadcast: null,
|
|
155
182
|
unsubPrompts: null,
|
|
183
|
+
unsubClaim: null,
|
|
184
|
+
activeClaimCode: null,
|
|
156
185
|
runtimeSnapshot: runtimeSnapshot ?? null,
|
|
157
186
|
dispose,
|
|
158
187
|
}
|
|
@@ -162,11 +191,11 @@ export function createServer({
|
|
|
162
191
|
await runtimeSnapshot.hooks.runSessionStart({ sessionId: sessionFileId, agentDir })
|
|
163
192
|
}
|
|
164
193
|
|
|
165
|
-
forwardSessionEvents(ws, session)
|
|
194
|
+
forwardSessionEvents(ws, session, logger, sessionFileId)
|
|
166
195
|
|
|
167
196
|
if (stream) {
|
|
168
197
|
state.unsubPrompts = stream.subscribe({ target: { kind: 'session', sessionId: sessionFileId } }, (msg) =>
|
|
169
|
-
enqueuePrompt(ws, state, msg, agentDir),
|
|
198
|
+
enqueuePrompt(ws, state, msg, agentDir, logger),
|
|
170
199
|
)
|
|
171
200
|
|
|
172
201
|
state.unsubBroadcast = stream.subscribe({ target: { kind: 'broadcast' } }, (msg) => {
|
|
@@ -198,6 +227,73 @@ export function createServer({
|
|
|
198
227
|
const msg = JSON.parse(String(raw)) as ClientMessage
|
|
199
228
|
const state = sessionStates.get(ws)
|
|
200
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
|
+
|
|
201
297
|
if (msg.type === 'reload') {
|
|
202
298
|
await handleReload(ws, reloadAll, reloadRegistry, msg.scope)
|
|
203
299
|
return
|
|
@@ -250,7 +346,9 @@ export function createServer({
|
|
|
250
346
|
await state.session.prompt(msg.text)
|
|
251
347
|
send(ws, { type: 'done' })
|
|
252
348
|
} catch (err) {
|
|
253
|
-
|
|
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 })
|
|
254
352
|
}
|
|
255
353
|
if (fallbackHooks !== undefined && agentDir !== undefined) {
|
|
256
354
|
await fallbackHooks.runSessionTurnEnd({
|
|
@@ -278,9 +376,13 @@ export function createServer({
|
|
|
278
376
|
const state = sessionStates.get(ws)
|
|
279
377
|
state?.unsubBroadcast?.()
|
|
280
378
|
state?.unsubPrompts?.()
|
|
379
|
+
state?.unsubClaim?.()
|
|
380
|
+
if (state?.activeClaimCode !== null && state?.activeClaimCode !== undefined && claimController) {
|
|
381
|
+
claimController.cancelClaim(state.activeClaimCode)
|
|
382
|
+
}
|
|
281
383
|
try {
|
|
282
384
|
if (state && state.runtimeSnapshot !== null) {
|
|
283
|
-
await state.runtimeSnapshot.hooks.runSessionEnd({ sessionId: state.sessionFileId })
|
|
385
|
+
await state.runtimeSnapshot.hooks.runSessionEnd({ sessionId: state.sessionFileId, origin: state.origin })
|
|
284
386
|
}
|
|
285
387
|
} finally {
|
|
286
388
|
if (state) {
|
|
@@ -305,7 +407,7 @@ function isWebSocketUpgrade(req: Request): boolean {
|
|
|
305
407
|
return req.headers.get('upgrade')?.toLowerCase() === 'websocket'
|
|
306
408
|
}
|
|
307
409
|
|
|
308
|
-
function forwardSessionEvents(ws: Ws, session: AgentSession): void {
|
|
410
|
+
function forwardSessionEvents(ws: Ws, session: AgentSession, logger: ServerLogger, sessionFileId: string): void {
|
|
309
411
|
const toolStartedAt = new Map<string, number>()
|
|
310
412
|
|
|
311
413
|
session.subscribe((event) => {
|
|
@@ -323,7 +425,7 @@ function forwardSessionEvents(ws: Ws, session: AgentSession): void {
|
|
|
323
425
|
// because no text deltas were ever emitted, which looks like a freeze.
|
|
324
426
|
// The server's existing try/catch around `session.prompt()` only
|
|
325
427
|
// catches throws, so it never sees these.
|
|
326
|
-
forwardAssistantError(ws, event.message)
|
|
428
|
+
forwardAssistantError(ws, event.message, logger, sessionFileId)
|
|
327
429
|
break
|
|
328
430
|
case 'tool_execution_start':
|
|
329
431
|
toolStartedAt.set(event.toolCallId, Date.now())
|
|
@@ -352,7 +454,7 @@ function forwardSessionEvents(ws: Ws, session: AgentSession): void {
|
|
|
352
454
|
})
|
|
353
455
|
}
|
|
354
456
|
|
|
355
|
-
function forwardAssistantError(ws: Ws, message: unknown): void {
|
|
457
|
+
function forwardAssistantError(ws: Ws, message: unknown, logger: ServerLogger, sessionFileId: string): void {
|
|
356
458
|
if (typeof message !== 'object' || message === null) return
|
|
357
459
|
const m = message as { role?: string; stopReason?: string; errorMessage?: string }
|
|
358
460
|
if (m.role !== 'assistant') return
|
|
@@ -361,10 +463,17 @@ function forwardAssistantError(ws: Ws, message: unknown): void {
|
|
|
361
463
|
// error message because the TUI already shows abort feedback elsewhere.
|
|
362
464
|
if (m.stopReason === 'aborted') return
|
|
363
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}`)
|
|
364
467
|
send(ws, { type: 'error', message: text })
|
|
365
468
|
}
|
|
366
469
|
|
|
367
|
-
function enqueuePrompt(
|
|
470
|
+
function enqueuePrompt(
|
|
471
|
+
ws: Ws,
|
|
472
|
+
state: SessionState,
|
|
473
|
+
msg: StreamMessage,
|
|
474
|
+
agentDir: string | undefined,
|
|
475
|
+
logger: ServerLogger,
|
|
476
|
+
): void {
|
|
368
477
|
const payload = msg.payload as { kind?: string; text?: string; delivery?: PromptDelivery }
|
|
369
478
|
if (payload?.kind !== 'prompt' || typeof payload.text !== 'string') return
|
|
370
479
|
const delivery: PromptDelivery = payload.delivery ?? 'queue'
|
|
@@ -380,7 +489,7 @@ function enqueuePrompt(ws: Ws, state: SessionState, msg: StreamMessage, agentDir
|
|
|
380
489
|
ts: msg.ts,
|
|
381
490
|
})
|
|
382
491
|
pushQueueState(ws, state)
|
|
383
|
-
void drain(ws, state, agentDir)
|
|
492
|
+
void drain(ws, state, agentDir, logger)
|
|
384
493
|
}
|
|
385
494
|
|
|
386
495
|
// `session.idle` semantically means "the agent finished a prompt and is now
|
|
@@ -416,7 +525,7 @@ function makeTurnHookCallers(
|
|
|
416
525
|
}
|
|
417
526
|
}
|
|
418
527
|
|
|
419
|
-
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> {
|
|
420
529
|
if (state.draining) return
|
|
421
530
|
state.draining = true
|
|
422
531
|
const fireIdle = makeIdleHookCaller(state)
|
|
@@ -433,7 +542,9 @@ async function drain(ws: Ws, state: SessionState, agentDir: string | undefined):
|
|
|
433
542
|
await state.session.prompt(item.text)
|
|
434
543
|
send(ws, { type: 'done' })
|
|
435
544
|
} catch (err) {
|
|
436
|
-
|
|
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 })
|
|
437
548
|
}
|
|
438
549
|
await fireTurnEnd()
|
|
439
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
|
|