typeclaw 0.1.5 → 0.2.0
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 +209 -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 +190 -61
- 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
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import type { KakaoAccountCredentials, KakaoConfig, PendingLoginState } from 'agent-messenger/kakaotalk'
|
|
15
15
|
|
|
16
16
|
import type { ChannelRouter } from '@/channels/router'
|
|
17
|
-
import {
|
|
17
|
+
import type { ChannelAdapterConfig } from '@/channels/schema'
|
|
18
18
|
import type {
|
|
19
19
|
ChannelHistoryMessage,
|
|
20
20
|
FetchHistoryArgs,
|
|
@@ -106,7 +106,7 @@ const consoleLogger: KakaotalkAdapterLogger = {
|
|
|
106
106
|
|
|
107
107
|
export type KakaotalkAdapterOptions = {
|
|
108
108
|
router: ChannelRouter
|
|
109
|
-
configRef: () =>
|
|
109
|
+
configRef: () => ChannelAdapterConfig
|
|
110
110
|
logger?: KakaotalkAdapterLogger
|
|
111
111
|
selfAliasesRef?: () => readonly string[]
|
|
112
112
|
credentialsStore?: KakaoCredentialStore
|
|
@@ -162,20 +162,14 @@ function formatLabel(name: string | undefined, id: string, prefix = ''): string
|
|
|
162
162
|
|
|
163
163
|
export function createOutboundCallback(deps: {
|
|
164
164
|
client: Pick<KakaoTalkClient, 'sendMessage'>
|
|
165
|
-
configRef: () => ChannelAdapterConfig
|
|
166
165
|
logger: KakaotalkAdapterLogger
|
|
167
166
|
formatChannelTag: (workspace: string, chat: string) => Promise<string>
|
|
168
167
|
}): OutboundCallback {
|
|
169
|
-
const { client,
|
|
168
|
+
const { client, logger, formatChannelTag } = deps
|
|
170
169
|
return async (msg: OutboundMessage): Promise<SendResult> => {
|
|
171
170
|
if (msg.adapter !== 'kakaotalk') {
|
|
172
171
|
return { ok: false, error: `unknown adapter: ${msg.adapter}` }
|
|
173
172
|
}
|
|
174
|
-
const config = configRef()
|
|
175
|
-
if (!isAllowed(config.allow, msg.workspace, msg.chat)) {
|
|
176
|
-
logger.warn(`[kakaotalk] outbound denied by allow rules: ${msg.workspace}/${msg.chat}`)
|
|
177
|
-
return { ok: false, error: 'denied by allow rules' }
|
|
178
|
-
}
|
|
179
173
|
const text = msg.text ?? ''
|
|
180
174
|
const attachments = msg.attachments ?? []
|
|
181
175
|
if (attachments.length > 0) {
|
|
@@ -214,27 +208,12 @@ export function createOutboundCallback(deps: {
|
|
|
214
208
|
|
|
215
209
|
export function createKakaoHistoryCallback(deps: {
|
|
216
210
|
client: Pick<KakaoTalkClient, 'getMessages'>
|
|
217
|
-
configRef: () => ChannelAdapterConfig
|
|
218
211
|
logger: KakaotalkAdapterLogger
|
|
219
|
-
channelResolver: Pick<KakaoChannelResolver, 'lookupChat' | 'refresh'>
|
|
220
212
|
authorResolver: Pick<KakaoAuthorResolver, 'resolve'>
|
|
221
213
|
selfUserIdRef: () => string | null
|
|
222
214
|
}): HistoryCallback {
|
|
223
|
-
const { client,
|
|
215
|
+
const { client, logger, authorResolver, selfUserIdRef } = deps
|
|
224
216
|
return async (args: FetchHistoryArgs): Promise<FetchHistoryResult> => {
|
|
225
|
-
const config = configRef()
|
|
226
|
-
let lookup = channelResolver.lookupChat(args.chat)
|
|
227
|
-
if (lookup === null) {
|
|
228
|
-
await channelResolver.refresh()
|
|
229
|
-
lookup = channelResolver.lookupChat(args.chat)
|
|
230
|
-
}
|
|
231
|
-
// Fallback to the most restrictive bucket (group) when the resolver
|
|
232
|
-
// can't classify after refresh — keeps allow-rule enforcement strict
|
|
233
|
-
// rather than defaulting to a permissive bucket.
|
|
234
|
-
const workspace = lookup?.workspace ?? '@kakao-group'
|
|
235
|
-
if (!isAllowed(config.allow, workspace, args.chat)) {
|
|
236
|
-
return { ok: false, error: 'denied by allow rules' }
|
|
237
|
-
}
|
|
238
217
|
const limit = clampLimit(args.limit, KAKAO_HISTORY_LIMIT_MAX)
|
|
239
218
|
try {
|
|
240
219
|
const messages = await client.getMessages(args.chat, {
|
|
@@ -311,16 +290,13 @@ export function createKakaotalkAdapter(options: KakaotalkAdapterOptions): Kakaot
|
|
|
311
290
|
|
|
312
291
|
const historyCallback = createKakaoHistoryCallback({
|
|
313
292
|
client,
|
|
314
|
-
configRef: options.configRef,
|
|
315
293
|
logger,
|
|
316
|
-
channelResolver,
|
|
317
294
|
authorResolver,
|
|
318
295
|
selfUserIdRef: () => selfUserId,
|
|
319
296
|
})
|
|
320
297
|
|
|
321
298
|
const outboundCallback = createOutboundCallback({
|
|
322
299
|
client,
|
|
323
|
-
configRef: options.configRef,
|
|
324
300
|
logger,
|
|
325
301
|
formatChannelTag,
|
|
326
302
|
})
|
|
@@ -390,10 +366,7 @@ export function createKakaotalkAdapter(options: KakaotalkAdapterOptions): Kakaot
|
|
|
390
366
|
...(options.selfAliasesRef ? { selfAliases: options.selfAliasesRef() } : {}),
|
|
391
367
|
})
|
|
392
368
|
if (verdict.kind === 'drop') {
|
|
393
|
-
|
|
394
|
-
logger.info(
|
|
395
|
-
`[kakaotalk] dropped log_id=${event.log_id} reason=${verdict.reason}${dropHint(verdict.reason, bucket, event.chat_id)}`,
|
|
396
|
-
)
|
|
369
|
+
logger.info(`[kakaotalk] dropped log_id=${event.log_id} reason=${verdict.reason}${dropHint(verdict.reason)}`)
|
|
397
370
|
return
|
|
398
371
|
}
|
|
399
372
|
|
|
@@ -462,6 +435,15 @@ export function createKakaotalkAdapter(options: KakaotalkAdapterOptions): Kakaot
|
|
|
462
435
|
logger.info(`[kakaotalk] authenticated as ${profile.nickname || profile.user_id} (${profile.user_id})`)
|
|
463
436
|
} catch (err) {
|
|
464
437
|
started = false
|
|
438
|
+
if (isKakaoUnauthorizedError(err)) {
|
|
439
|
+
const message =
|
|
440
|
+
'KakaoTalk sub-device session is stale (server returned 401 on getProfile). ' +
|
|
441
|
+
'This usually means the ~7-day token TTL has expired and the hostd renewal cron has not refreshed it yet — ' +
|
|
442
|
+
'either because the agent was just initialized without stored credentials, or because the encryption key ' +
|
|
443
|
+
'is missing/wrong. Run `typeclaw channel reauth kakaotalk` to mint fresh tokens, then `typeclaw reload`.'
|
|
444
|
+
logger.error(`[kakaotalk] ${message}`)
|
|
445
|
+
throw new Error(message)
|
|
446
|
+
}
|
|
465
447
|
logger.error(`[kakaotalk] getProfile failed: ${describe(err)}`)
|
|
466
448
|
throw err
|
|
467
449
|
}
|
|
@@ -525,7 +507,8 @@ export function createKakaotalkAdapter(options: KakaotalkAdapterOptions): Kakaot
|
|
|
525
507
|
`[kakaotalk] session is DEAD after KICKOUT — ${reason}. ` +
|
|
526
508
|
'Likely a real cross-device login is fighting our session. ' +
|
|
527
509
|
'Stop the other client, then run `typeclaw restart`. ' +
|
|
528
|
-
'If the conflict persists,
|
|
510
|
+
'If the conflict persists, run `typeclaw channel reauth kakaotalk` to mint a fresh sub-device session ' +
|
|
511
|
+
'(the existing device_uuid is preserved by default so the new login skips phone-passcode confirmation).',
|
|
529
512
|
)
|
|
530
513
|
resetRecoveryEpisode()
|
|
531
514
|
return
|
|
@@ -656,14 +639,8 @@ function markReadIfSupported(deps: {
|
|
|
656
639
|
)
|
|
657
640
|
}
|
|
658
641
|
|
|
659
|
-
function dropHint(
|
|
660
|
-
reason: InboundDropReason,
|
|
661
|
-
bucket: '@kakao-dm' | '@kakao-group' | '@kakao-open' | null,
|
|
662
|
-
chatId: string,
|
|
663
|
-
): string {
|
|
642
|
+
function dropHint(reason: InboundDropReason): string {
|
|
664
643
|
switch (reason) {
|
|
665
|
-
case 'not_in_allow_list':
|
|
666
|
-
return ` (add ${suggestedAllowPattern(bucket, chatId)} to channels.kakaotalk.allow to admit this chat)`
|
|
667
644
|
case 'unknown_chat':
|
|
668
645
|
return ' (chat not in cache after refresh and provisional registration; check earlier resolver-refresh-failed warnings)'
|
|
669
646
|
case 'empty_text':
|
|
@@ -673,14 +650,18 @@ function dropHint(
|
|
|
673
650
|
}
|
|
674
651
|
}
|
|
675
652
|
|
|
676
|
-
function suggestedAllowPattern(bucket: '@kakao-dm' | '@kakao-group' | '@kakao-open' | null, chatId: string): string {
|
|
677
|
-
if (bucket === '@kakao-dm') return `"kakao:dm/*" or "kakao:${chatId}"`
|
|
678
|
-
if (bucket === '@kakao-group') return `"kakao:group/*" or "kakao:${chatId}"`
|
|
679
|
-
if (bucket === '@kakao-open') return `"kakao:open/*" or "kakao:${chatId}"`
|
|
680
|
-
return `"kakao:${chatId}"`
|
|
681
|
-
}
|
|
682
|
-
|
|
683
653
|
function isKickoutError(err: unknown): boolean {
|
|
684
654
|
if (!(err instanceof Error)) return false
|
|
685
655
|
return err.message.includes('kicked') || err.message.includes('KICKOUT')
|
|
686
656
|
}
|
|
657
|
+
|
|
658
|
+
// String-match on agent-messenger's `Profile request failed: ${status}`
|
|
659
|
+
// error format (see kakaotalk/client.js:544). The SDK throws KakaoTalkError
|
|
660
|
+
// with code='profile_request_failed' for any non-2xx status, so we have to
|
|
661
|
+
// inspect the message to tell 401 (expired sub-device token, needs renewal)
|
|
662
|
+
// apart from 5xx (transient server issue). Until the SDK exposes a typed
|
|
663
|
+
// `unauthorized` code, this is the realistic detection path.
|
|
664
|
+
function isKakaoUnauthorizedError(err: unknown): boolean {
|
|
665
|
+
if (!(err instanceof Error)) return false
|
|
666
|
+
return /Profile request failed: 401\b/.test(err.message)
|
|
667
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SlackFile, SlackSocketModeAppMentionEvent, SlackSocketModeMessageEvent } from 'agent-messenger/slackbot'
|
|
2
2
|
|
|
3
3
|
import { matchesAnyAlias } from '@/channels/engagement'
|
|
4
|
-
import {
|
|
4
|
+
import type { ChannelAdapterConfig } from '@/channels/schema'
|
|
5
5
|
import type { InboundMessage } from '@/channels/types'
|
|
6
6
|
|
|
7
7
|
import { slackTsToMillis } from './slack-bot-time'
|
|
@@ -38,7 +38,6 @@ export type InboundDropReason =
|
|
|
38
38
|
| 'self_author' // event.user === botUserId; we never route our own messages back to ourselves
|
|
39
39
|
| 'no_user' // event has no `user` field (e.g. system messages: channel_join, message_changed)
|
|
40
40
|
| 'empty_text' // event has neither text nor files — nothing for the agent to act on
|
|
41
|
-
| 'not_in_allow_list' // workspace/channel not admitted by typeclaw.json `channels.slack-bot.allow`
|
|
42
41
|
| 'pre_connect' // bot identity is not known yet, so mention/self/reply classification cannot be trusted
|
|
43
42
|
|
|
44
43
|
export type InboundClassification =
|
|
@@ -67,7 +66,7 @@ export type SlackInboundContext = {
|
|
|
67
66
|
// forces logging to stay exhaustive.
|
|
68
67
|
export function classifyInbound(
|
|
69
68
|
event: SlackInboundMessageEvent,
|
|
70
|
-
|
|
69
|
+
_config: ChannelAdapterConfig,
|
|
71
70
|
context: SlackInboundContext,
|
|
72
71
|
): InboundClassification {
|
|
73
72
|
// Self-drop is the hard floor: never route our own messages back to
|
|
@@ -92,9 +91,6 @@ export function classifyInbound(
|
|
|
92
91
|
|
|
93
92
|
const isDm = event.channel_type === 'im'
|
|
94
93
|
const workspace = isDm ? '@dm' : context.teamId
|
|
95
|
-
if (!isAllowed(config.allow, workspace, event.channel)) {
|
|
96
|
-
return { kind: 'drop', reason: 'not_in_allow_list' }
|
|
97
|
-
}
|
|
98
94
|
|
|
99
95
|
if (context.botUserId === null) {
|
|
100
96
|
return { kind: 'drop', reason: 'pre_connect' }
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from '@/channels/membership'
|
|
9
9
|
import { deriveMembershipFromHistory } from '@/channels/membership-from-history'
|
|
10
10
|
import type { ChannelRouter } from '@/channels/router'
|
|
11
|
-
import {
|
|
11
|
+
import type { ChannelAdapterConfig } from '@/channels/schema'
|
|
12
12
|
import type {
|
|
13
13
|
ChannelHistoryMessage,
|
|
14
14
|
FetchAttachmentCallback,
|
|
@@ -170,15 +170,12 @@ export function createSlackTypingTracker(deps: {
|
|
|
170
170
|
|
|
171
171
|
export function createTypingCallback(deps: {
|
|
172
172
|
typingTracker: Pick<SlackTypingTracker, 'setStatus' | 'clearAfterSend'>
|
|
173
|
-
configRef: () => ChannelAdapterConfig
|
|
174
173
|
logger: SlackBotAdapterLogger
|
|
175
174
|
formatChannelTag?: (workspace: string, chat: string) => Promise<string>
|
|
176
175
|
}): TypingCallback {
|
|
177
|
-
const { typingTracker,
|
|
176
|
+
const { typingTracker, logger, formatChannelTag } = deps
|
|
178
177
|
return async (target: TypingTarget): Promise<void> => {
|
|
179
178
|
if (target.adapter !== 'slack-bot') return
|
|
180
|
-
const config = configRef()
|
|
181
|
-
if (!isAllowed(config.allow, target.workspace, target.chat)) return
|
|
182
179
|
const tag = formatChannelTag
|
|
183
180
|
? await formatChannelTag(target.workspace, target.thread ?? target.chat)
|
|
184
181
|
: `channel=${target.thread ?? target.chat}`
|
|
@@ -370,23 +367,13 @@ function slackFailureForError(error: string): MembershipResolverFailure {
|
|
|
370
367
|
// and is the most-tested wire format.
|
|
371
368
|
export function createSlackHistoryCallback(deps: {
|
|
372
369
|
token: string
|
|
373
|
-
configRef: () => ChannelAdapterConfig
|
|
374
370
|
logger: SlackBotAdapterLogger
|
|
375
371
|
botUserIdRef: () => string | null
|
|
376
372
|
fetchImpl?: typeof fetch
|
|
377
373
|
}): HistoryCallback {
|
|
378
|
-
const { token,
|
|
374
|
+
const { token, logger, botUserIdRef } = deps
|
|
379
375
|
const fetchFn = deps.fetchImpl ?? fetch
|
|
380
376
|
return async (args: FetchHistoryArgs): Promise<FetchHistoryResult> => {
|
|
381
|
-
const config = configRef()
|
|
382
|
-
if (!isAllowed(config.allow, '@dm', args.chat) && !isAllowedAnyTeam(config.allow, args.chat)) {
|
|
383
|
-
// Same defense-in-depth as outbound: refuse to fetch history for a
|
|
384
|
-
// channel the operator hasn't admitted, even if the agent somehow
|
|
385
|
-
// resolved its id. Returning an error rather than empty so the
|
|
386
|
-
// agent doesn't think the channel is genuinely silent.
|
|
387
|
-
return { ok: false, error: 'denied by allow rules' }
|
|
388
|
-
}
|
|
389
|
-
|
|
390
377
|
const limit = clampLimit(args.limit, SLACK_HISTORY_LIMIT_MAX)
|
|
391
378
|
const endpoint = args.thread === null ? 'conversations.history' : 'conversations.replies'
|
|
392
379
|
const body = new URLSearchParams()
|
|
@@ -465,25 +452,6 @@ function clampLimit(requested: number, max: number): number {
|
|
|
465
452
|
return Math.min(Math.floor(requested), max)
|
|
466
453
|
}
|
|
467
454
|
|
|
468
|
-
// Slack channel ids are globally unique on Slack's side, so a `channel:C…`
|
|
469
|
-
// or `team:T/C` rule for any team admits this chat. We use this for the
|
|
470
|
-
// history allow check because at fetch time we only know the channel id,
|
|
471
|
-
// not the workspace (the tool resolves the chat from session origin and
|
|
472
|
-
// the workspace doesn't always round-trip through cursor pagination).
|
|
473
|
-
function isAllowedAnyTeam(rules: readonly string[], chat: string): boolean {
|
|
474
|
-
for (const rule of rules) {
|
|
475
|
-
if (rule === '*') return true
|
|
476
|
-
if (rule === 'team:*' || rule === 'guild:*') return true
|
|
477
|
-
if (rule.startsWith('channel:') && rule.slice(8) === chat) return true
|
|
478
|
-
if (rule.startsWith('team:')) {
|
|
479
|
-
const body = rule.slice(5)
|
|
480
|
-
const slash = body.indexOf('/')
|
|
481
|
-
if (slash !== -1 && body.slice(slash + 1) === chat) return true
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return false
|
|
485
|
-
}
|
|
486
|
-
|
|
487
455
|
// Slack supports text+file in a single API call via `initial_comment`, and
|
|
488
456
|
// honors `thread_ts` on every upload — both luxuries Discord lacks. So we
|
|
489
457
|
// fold `text` into the FIRST attachment's `initial_comment` rather than
|
|
@@ -528,23 +496,17 @@ function buildMarkdownBlock(text: string): MarkdownBlock {
|
|
|
528
496
|
|
|
529
497
|
export function createOutboundCallback(deps: {
|
|
530
498
|
client: Pick<SlackBotClient, 'postMessage' | 'uploadFile'>
|
|
531
|
-
configRef: () => ChannelAdapterConfig
|
|
532
499
|
logger: SlackBotAdapterLogger
|
|
533
500
|
formatChannelTag: (workspace: string, chat: string) => Promise<string>
|
|
534
501
|
readFile?: (path: string) => Promise<Buffer>
|
|
535
502
|
typingTracker?: Pick<SlackTypingTracker, 'clearAfterSend'>
|
|
536
503
|
}): OutboundCallback {
|
|
537
|
-
const { client,
|
|
504
|
+
const { client, logger, formatChannelTag, typingTracker } = deps
|
|
538
505
|
const readFile = deps.readFile ?? readAttachmentBuffer
|
|
539
506
|
return async (msg: OutboundMessage): Promise<SendResult> => {
|
|
540
507
|
if (msg.adapter !== 'slack-bot') {
|
|
541
508
|
return { ok: false, error: `unknown adapter: ${msg.adapter}` }
|
|
542
509
|
}
|
|
543
|
-
const config = configRef()
|
|
544
|
-
if (!isAllowed(config.allow, msg.workspace, msg.chat)) {
|
|
545
|
-
logger.warn(`[slack-bot] outbound denied by allow rules: ${msg.workspace}/${msg.chat}`)
|
|
546
|
-
return { ok: false, error: 'denied by allow rules' }
|
|
547
|
-
}
|
|
548
510
|
const text = msg.text ?? ''
|
|
549
511
|
const attachments = msg.attachments ?? []
|
|
550
512
|
if (text === '' && attachments.length === 0) {
|
|
@@ -672,14 +634,12 @@ export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBot
|
|
|
672
634
|
|
|
673
635
|
const typingCallback = createTypingCallback({
|
|
674
636
|
typingTracker,
|
|
675
|
-
configRef: options.configRef,
|
|
676
637
|
logger,
|
|
677
638
|
formatChannelTag,
|
|
678
639
|
})
|
|
679
640
|
|
|
680
641
|
const historyCallback = createSlackHistoryCallback({
|
|
681
642
|
token: options.token,
|
|
682
|
-
configRef: options.configRef,
|
|
683
643
|
logger,
|
|
684
644
|
botUserIdRef: () => botUserId,
|
|
685
645
|
})
|
|
@@ -692,7 +652,6 @@ export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBot
|
|
|
692
652
|
|
|
693
653
|
const outboundCallback = createOutboundCallback({
|
|
694
654
|
client,
|
|
695
|
-
configRef: options.configRef,
|
|
696
655
|
logger,
|
|
697
656
|
formatChannelTag,
|
|
698
657
|
typingTracker,
|
|
@@ -865,13 +824,8 @@ function describe(err: unknown): string {
|
|
|
865
824
|
return err instanceof Error ? err.message : String(err)
|
|
866
825
|
}
|
|
867
826
|
|
|
868
|
-
// Operator hints appended to drop logs. Kept short — full guidance lives in
|
|
869
|
-
// docs. The not_in_allow_list hint is the highest-leverage one because that
|
|
870
|
-
// failure mode is invisible from Slack's side (bot stays online).
|
|
871
827
|
function dropHint(reason: InboundDropReason): string {
|
|
872
828
|
switch (reason) {
|
|
873
|
-
case 'not_in_allow_list':
|
|
874
|
-
return ' (extend channels.slack-bot.allow in typeclaw.json to admit this team/channel)'
|
|
875
829
|
case 'empty_text':
|
|
876
830
|
case 'no_user':
|
|
877
831
|
case 'pre_connect':
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { TelegramBotUser, TelegramMessage, TelegramMessageEntity } from 'agent-messenger/telegrambot'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { ChannelAdapterConfig } from '@/channels/schema'
|
|
4
4
|
import type { InboundMessage } from '@/channels/types'
|
|
5
5
|
|
|
6
|
-
export type InboundDropReason = 'self_author' | 'no_user' | 'empty_text' | '
|
|
6
|
+
export type InboundDropReason = 'self_author' | 'no_user' | 'empty_text' | 'pre_connect'
|
|
7
7
|
|
|
8
8
|
export type InboundClassification =
|
|
9
9
|
| { kind: 'drop'; reason: InboundDropReason }
|
|
@@ -13,13 +13,14 @@ export const TELEGRAM_WORKSPACE = 'telegram'
|
|
|
13
13
|
|
|
14
14
|
// Telegram has no team/guild concept — every chat is identified by an
|
|
15
15
|
// absolute (signed) numeric id. We pin `workspace` to a single bucket so
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// match rules like `telegram:*` and `telegram:<chat_id>` resolve against
|
|
17
|
+
// a stable key downstream in the permissions service. DMs use `private`
|
|
18
|
+
// chats and route the same way as group chats from the router's
|
|
19
|
+
// perspective; `isDm` is set from `chat.type` so the engagement layer
|
|
20
|
+
// can apply the DM-specific trigger.
|
|
20
21
|
export function classifyInbound(
|
|
21
22
|
event: TelegramMessage,
|
|
22
|
-
|
|
23
|
+
_config: ChannelAdapterConfig,
|
|
23
24
|
bot: TelegramBotUser | null,
|
|
24
25
|
): InboundClassification {
|
|
25
26
|
const author = event.from
|
|
@@ -34,9 +35,6 @@ export function classifyInbound(
|
|
|
34
35
|
if (text === '') return { kind: 'drop', reason: 'empty_text' }
|
|
35
36
|
|
|
36
37
|
const chat = String(event.chat.id)
|
|
37
|
-
if (!isAllowed(config.allow, TELEGRAM_WORKSPACE, chat)) {
|
|
38
|
-
return { kind: 'drop', reason: 'not_in_allow_list' }
|
|
39
|
-
}
|
|
40
38
|
|
|
41
39
|
if (bot === null) {
|
|
42
40
|
return { kind: 'drop', reason: 'pre_connect' }
|
|
@@ -3,7 +3,7 @@ import type { TelegramBotUser, TelegramMessage } from 'agent-messenger/telegramb
|
|
|
3
3
|
|
|
4
4
|
import type { MembershipResolver, MembershipResolverFailure, MembershipResolverResult } from '@/channels/membership'
|
|
5
5
|
import type { ChannelRouter } from '@/channels/router'
|
|
6
|
-
import {
|
|
6
|
+
import type { ChannelAdapterConfig } from '@/channels/schema'
|
|
7
7
|
import type {
|
|
8
8
|
ChannelNameResolver,
|
|
9
9
|
FetchAttachmentCallback,
|
|
@@ -77,12 +77,11 @@ export type TelegramBotAdapter = {
|
|
|
77
77
|
|
|
78
78
|
export function createTypingCallback(deps: {
|
|
79
79
|
token: string
|
|
80
|
-
configRef: () => ChannelAdapterConfig
|
|
81
80
|
logger: TelegramBotAdapterLogger
|
|
82
81
|
formatChannelTag?: (chat: string) => Promise<string>
|
|
83
82
|
fetchImpl?: typeof fetch
|
|
84
83
|
}): TypingCallback {
|
|
85
|
-
const { token,
|
|
84
|
+
const { token, logger, formatChannelTag } = deps
|
|
86
85
|
const fetchImpl = deps.fetchImpl ?? fetch
|
|
87
86
|
return async (target: TypingTarget): Promise<void> => {
|
|
88
87
|
if (target.adapter !== 'telegram-bot') return
|
|
@@ -91,8 +90,6 @@ export function createTypingCallback(deps: {
|
|
|
91
90
|
// a missed beat just gaps the indicator. There is no explicit clear,
|
|
92
91
|
// so the 'stop' phase is a no-op.
|
|
93
92
|
if (target.phase === 'stop') return
|
|
94
|
-
const config = configRef()
|
|
95
|
-
if (!isAllowed(config.allow, target.workspace, target.chat)) return
|
|
96
93
|
const tag = formatChannelTag ? await formatChannelTag(target.chat) : `chat=${target.chat}`
|
|
97
94
|
const body: Record<string, unknown> = { chat_id: target.chat, action: 'typing' }
|
|
98
95
|
const threadId = parseThreadId(target.thread)
|
|
@@ -197,21 +194,15 @@ export function createTelegramMembershipResolver(deps: {
|
|
|
197
194
|
|
|
198
195
|
export function createOutboundCallback(deps: {
|
|
199
196
|
client: Pick<TelegramBotClient, 'sendMessage' | 'sendDocument'>
|
|
200
|
-
configRef: () => ChannelAdapterConfig
|
|
201
197
|
logger: TelegramBotAdapterLogger
|
|
202
198
|
formatChannelTag: (chat: string) => Promise<string>
|
|
203
199
|
resolvePath?: (path: string) => string
|
|
204
200
|
}): OutboundCallback {
|
|
205
|
-
const { client,
|
|
201
|
+
const { client, logger, formatChannelTag, resolvePath } = deps
|
|
206
202
|
return async (msg: OutboundMessage): Promise<SendResult> => {
|
|
207
203
|
if (msg.adapter !== 'telegram-bot') {
|
|
208
204
|
return { ok: false, error: `unknown adapter: ${msg.adapter}` }
|
|
209
205
|
}
|
|
210
|
-
const config = configRef()
|
|
211
|
-
if (!isAllowed(config.allow, msg.workspace, msg.chat)) {
|
|
212
|
-
logger.warn(`[telegram-bot] outbound denied by allow rules: ${msg.workspace}/${msg.chat}`)
|
|
213
|
-
return { ok: false, error: 'denied by allow rules' }
|
|
214
|
-
}
|
|
215
206
|
const text = msg.text ?? ''
|
|
216
207
|
const attachments = msg.attachments ?? []
|
|
217
208
|
if (text === '' && attachments.length === 0) {
|
|
@@ -396,7 +387,6 @@ export function createTelegramBotAdapter(options: TelegramBotAdapterOptions): Te
|
|
|
396
387
|
|
|
397
388
|
const typingCallback = createTypingCallback({
|
|
398
389
|
token: options.token,
|
|
399
|
-
configRef: options.configRef,
|
|
400
390
|
logger,
|
|
401
391
|
formatChannelTag,
|
|
402
392
|
})
|
|
@@ -405,7 +395,6 @@ export function createTelegramBotAdapter(options: TelegramBotAdapterOptions): Te
|
|
|
405
395
|
|
|
406
396
|
const outboundCallback = createOutboundCallback({
|
|
407
397
|
client,
|
|
408
|
-
configRef: options.configRef,
|
|
409
398
|
logger,
|
|
410
399
|
formatChannelTag,
|
|
411
400
|
})
|
|
@@ -595,8 +584,6 @@ function dropHint(reason: InboundDropReason): string {
|
|
|
595
584
|
return ' (channel post / anonymous; cannot attribute to an author)'
|
|
596
585
|
case 'empty_text':
|
|
597
586
|
return ' (message had no text and no recognized media; check Telegram privacy mode in @BotFather)'
|
|
598
|
-
case 'not_in_allow_list':
|
|
599
|
-
return ' (extend channels.telegram-bot.allow in typeclaw.json to admit this chat)'
|
|
600
587
|
case 'pre_connect':
|
|
601
588
|
case 'self_author':
|
|
602
589
|
return ''
|
package/src/channels/index.ts
CHANGED
|
@@ -2,17 +2,18 @@ export { createChannelManager, type ChannelManager, type ChannelManagerOptions }
|
|
|
2
2
|
export {
|
|
3
3
|
createChannelRouter,
|
|
4
4
|
type ChannelRouter,
|
|
5
|
+
type ClaimHandler,
|
|
6
|
+
type ClaimHandlerInput,
|
|
7
|
+
type ClaimHandlerOutcome,
|
|
5
8
|
type CreateChannelRouterOptions,
|
|
6
9
|
type CreateSessionForChannel,
|
|
7
10
|
} from './router'
|
|
8
11
|
export { createChannelsReloadable } from './reloadable'
|
|
9
12
|
export {
|
|
10
13
|
channelsSchema,
|
|
11
|
-
isAllowed,
|
|
12
14
|
ADAPTER_IDS,
|
|
13
15
|
STICKY_DEFAULT_WINDOW_MS,
|
|
14
16
|
type AdapterId,
|
|
15
|
-
type AllowRule,
|
|
16
17
|
type ChannelAdapterConfig,
|
|
17
18
|
type ChannelsConfig,
|
|
18
19
|
type EngagementConfig,
|
package/src/channels/manager.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
|
+
import type { PermissionService } from '@/permissions'
|
|
4
5
|
import { SecretsKakaoCredentialStore } from '@/secrets/kakao-store'
|
|
5
6
|
import { SecretsBackend } from '@/secrets/storage'
|
|
6
7
|
|
|
@@ -8,7 +9,7 @@ import { createDiscordBotAdapter, type DiscordBotAdapter } from './adapters/disc
|
|
|
8
9
|
import { createKakaotalkAdapter, type KakaotalkAdapter } from './adapters/kakaotalk'
|
|
9
10
|
import { createSlackBotAdapter, type SlackBotAdapter } from './adapters/slack-bot'
|
|
10
11
|
import { createTelegramBotAdapter, type TelegramBotAdapter } from './adapters/telegram-bot'
|
|
11
|
-
import { createChannelRouter, type ChannelRouter, type CreateSessionForChannel } from './router'
|
|
12
|
+
import { createChannelRouter, type ChannelRouter, type ClaimHandler, type CreateSessionForChannel } from './router'
|
|
12
13
|
import { ADAPTER_IDS, type AdapterId, type ChannelAdapterConfig, type ChannelsConfig } from './schema'
|
|
13
14
|
|
|
14
15
|
export type ChannelManagerLogger = {
|
|
@@ -50,6 +51,17 @@ export type ChannelManagerOptions = {
|
|
|
50
51
|
createKakaotalkAdapter?: typeof createKakaotalkAdapter
|
|
51
52
|
createSlackAdapter?: typeof createSlackBotAdapter
|
|
52
53
|
createTelegramAdapter?: typeof createTelegramBotAdapter
|
|
54
|
+
// Wake-up gate: forwarded to the router, which calls
|
|
55
|
+
// `permissions.has(origin, 'channel.respond')` BEFORE creating a
|
|
56
|
+
// session for any inbound. Optional here to keep direct manager-level
|
|
57
|
+
// tests easy to spin up; production wiring in src/run/index.ts always
|
|
58
|
+
// passes `pluginsLoaded.permissions`. Omitting it falls through to the
|
|
59
|
+
// router's grant-all default — see CreateChannelRouterOptions.
|
|
60
|
+
permissions?: PermissionService
|
|
61
|
+
// Forwarded to the router; intercepts DM inbounds carrying a role-claim
|
|
62
|
+
// code. Production wiring sets this from the role-claim subsystem (see
|
|
63
|
+
// src/run/index.ts). Tests typically omit it.
|
|
64
|
+
claimHandler?: ClaimHandler
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
export type ChannelManager = {
|
|
@@ -82,6 +94,8 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
82
94
|
logger,
|
|
83
95
|
...(options.aliasesRef ? { configuredAliases: options.aliasesRef } : {}),
|
|
84
96
|
...(options.createSessionForChannel ? { createSessionForChannel: options.createSessionForChannel } : {}),
|
|
97
|
+
...(options.permissions ? { permissions: options.permissions } : {}),
|
|
98
|
+
...(options.claimHandler ? { claimHandler: options.claimHandler } : {}),
|
|
85
99
|
})
|
|
86
100
|
const createDiscordAdapter = options.createDiscordAdapter ?? createDiscordBotAdapter
|
|
87
101
|
const createKakaotalk = options.createKakaotalkAdapter ?? createKakaotalkAdapter
|
|
@@ -6,7 +6,7 @@ import type { ChannelParticipant } from '@/agent/session-origin'
|
|
|
6
6
|
import type { AdapterId } from './schema'
|
|
7
7
|
import type { ChannelKey } from './types'
|
|
8
8
|
|
|
9
|
-
const FILE_VERSION =
|
|
9
|
+
const FILE_VERSION = 4
|
|
10
10
|
|
|
11
11
|
// `sessionFile` is the basename (not the full path) of the JSONL transcript
|
|
12
12
|
// for this (adapter, workspace, chat, thread) tuple. pi-coding-agent writes
|
|
@@ -25,11 +25,17 @@ export type ChannelSessionRecord = {
|
|
|
25
25
|
workspace: string
|
|
26
26
|
chat: string
|
|
27
27
|
thread: string | null
|
|
28
|
-
sessionId
|
|
28
|
+
sessionId?: string
|
|
29
29
|
sessionFile?: string
|
|
30
|
+
lastInboundAt?: number
|
|
30
31
|
participants: ChannelParticipant[]
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
type FileV4 = {
|
|
35
|
+
version: 4
|
|
36
|
+
sessions: ChannelSessionRecord[]
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
type FileV3 = {
|
|
34
40
|
version: 3
|
|
35
41
|
sessions: ChannelSessionRecord[]
|
|
@@ -41,11 +47,13 @@ type FileV2 = {
|
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
export type ChannelSessionsLogger = {
|
|
50
|
+
info: (msg: string) => void
|
|
44
51
|
warn: (msg: string) => void
|
|
45
52
|
error: (msg: string) => void
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
const consoleLogger: ChannelSessionsLogger = {
|
|
56
|
+
info: (m) => console.log(m),
|
|
49
57
|
warn: (m) => console.warn(m),
|
|
50
58
|
error: (m) => console.error(m),
|
|
51
59
|
}
|
|
@@ -82,17 +90,25 @@ export async function loadChannelSessions(
|
|
|
82
90
|
}
|
|
83
91
|
const version = (parsed as { version?: unknown }).version
|
|
84
92
|
if (version === FILE_VERSION) {
|
|
85
|
-
const file = parsed as
|
|
93
|
+
const file = parsed as FileV4
|
|
86
94
|
if (!Array.isArray(file.sessions)) return []
|
|
87
95
|
return file.sessions.filter(isValidRecord)
|
|
88
96
|
}
|
|
97
|
+
if (version === 3) {
|
|
98
|
+
const file = parsed as FileV3
|
|
99
|
+
if (!Array.isArray(file.sessions)) return []
|
|
100
|
+
return migrateV3ToV4(file.sessions.filter(isValidRecord), logger)
|
|
101
|
+
}
|
|
89
102
|
if (version === 2) {
|
|
90
103
|
const file = parsed as FileV2
|
|
91
104
|
if (!Array.isArray(file.sessions)) return []
|
|
92
105
|
const v2Records = file.sessions.filter(isValidV2Record)
|
|
93
|
-
|
|
106
|
+
const v3Records = await migrateV2Records(agentDir, v2Records, logger)
|
|
107
|
+
return migrateV3ToV4(v3Records, logger)
|
|
94
108
|
}
|
|
95
|
-
logger.warn(
|
|
109
|
+
logger.warn(
|
|
110
|
+
`[channels] ${path} version ${String(version)} not supported (expected 2, 3, or ${FILE_VERSION}); ignored`,
|
|
111
|
+
)
|
|
96
112
|
return []
|
|
97
113
|
}
|
|
98
114
|
|
|
@@ -102,7 +118,7 @@ export async function saveChannelSessions(
|
|
|
102
118
|
logger: ChannelSessionsLogger = consoleLogger,
|
|
103
119
|
): Promise<void> {
|
|
104
120
|
const path = channelsSessionsPath(agentDir)
|
|
105
|
-
const payload:
|
|
121
|
+
const payload: FileV4 = { version: FILE_VERSION, sessions: dedupe(sessions) }
|
|
106
122
|
try {
|
|
107
123
|
await mkdir(dirname(path), { recursive: true })
|
|
108
124
|
const tmp = `${path}.tmp`
|
|
@@ -123,7 +139,7 @@ export async function saveChannelSessions(
|
|
|
123
139
|
// we'll be migrated forward.)
|
|
124
140
|
async function migrateV2Records(
|
|
125
141
|
agentDir: string,
|
|
126
|
-
v2Records: readonly Omit<ChannelSessionRecord, 'sessionFile'>[],
|
|
142
|
+
v2Records: readonly (Omit<ChannelSessionRecord, 'sessionFile' | 'sessionId'> & { sessionId: string })[],
|
|
127
143
|
logger: ChannelSessionsLogger,
|
|
128
144
|
): Promise<ChannelSessionRecord[]> {
|
|
129
145
|
if (v2Records.length === 0) return []
|
|
@@ -160,6 +176,13 @@ async function migrateV2Records(
|
|
|
160
176
|
})
|
|
161
177
|
}
|
|
162
178
|
|
|
179
|
+
function migrateV3ToV4(v3Records: ChannelSessionRecord[], logger: ChannelSessionsLogger): ChannelSessionRecord[] {
|
|
180
|
+
logger.info(
|
|
181
|
+
`[channels] v3→v4: ${v3Records.length} record(s) migrated; first post-upgrade inbound will force fresh session`,
|
|
182
|
+
)
|
|
183
|
+
return v3Records.map((r) => ({ ...r, lastInboundAt: 0 }))
|
|
184
|
+
}
|
|
185
|
+
|
|
163
186
|
function dedupe(sessions: readonly ChannelSessionRecord[]): ChannelSessionRecord[] {
|
|
164
187
|
const seen = new Map<string, ChannelSessionRecord>()
|
|
165
188
|
for (const s of sessions) {
|
|
@@ -185,7 +208,9 @@ function isObject(v: unknown): v is Record<string, unknown> {
|
|
|
185
208
|
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
186
209
|
}
|
|
187
210
|
|
|
188
|
-
function isValidV2Record(
|
|
211
|
+
function isValidV2Record(
|
|
212
|
+
v: unknown,
|
|
213
|
+
): v is Omit<ChannelSessionRecord, 'sessionFile' | 'sessionId'> & { sessionId: string } {
|
|
189
214
|
if (!isObject(v)) return false
|
|
190
215
|
const r = v as Record<string, unknown>
|
|
191
216
|
return (
|
|
@@ -199,9 +224,18 @@ function isValidV2Record(v: unknown): v is Omit<ChannelSessionRecord, 'sessionFi
|
|
|
199
224
|
}
|
|
200
225
|
|
|
201
226
|
function isValidRecord(v: unknown): v is ChannelSessionRecord {
|
|
202
|
-
if (!
|
|
227
|
+
if (!isObject(v)) return false
|
|
203
228
|
const r = v as Record<string, unknown>
|
|
204
|
-
return
|
|
229
|
+
return (
|
|
230
|
+
typeof r.adapter === 'string' &&
|
|
231
|
+
typeof r.workspace === 'string' &&
|
|
232
|
+
typeof r.chat === 'string' &&
|
|
233
|
+
(r.thread === null || typeof r.thread === 'string') &&
|
|
234
|
+
(r.sessionId === undefined || typeof r.sessionId === 'string') &&
|
|
235
|
+
(r.sessionFile === undefined || typeof r.sessionFile === 'string') &&
|
|
236
|
+
(r.lastInboundAt === undefined || typeof r.lastInboundAt === 'number') &&
|
|
237
|
+
Array.isArray(r.participants)
|
|
238
|
+
)
|
|
205
239
|
}
|
|
206
240
|
|
|
207
241
|
function describe(err: unknown): string {
|