typeclaw 0.37.3 → 0.37.4
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 +69 -46
- package/package.json +1 -1
- package/src/agent/compaction.ts +24 -15
- package/src/agent/session-origin.ts +101 -173
- package/src/agent/system-prompt.ts +46 -48
- package/src/bundled-plugins/memory/index.ts +24 -27
- package/src/bundled-plugins/memory/load-memory.ts +78 -35
- package/src/bundled-plugins/memory/turn-dedup.ts +32 -29
- package/src/bundled-plugins/tool-result-cap/README.md +7 -7
- package/src/bundled-plugins/tool-result-cap/index.ts +1 -1
- package/src/channels/adapters/discord-bot.ts +11 -4
- package/src/channels/adapters/mention-hints.ts +58 -0
- package/src/channels/adapters/slack-bot.ts +8 -2
- package/src/channels/continuation-willingness.ts +216 -68
- package/src/channels/router.ts +29 -3
- package/src/cli/init.ts +41 -7
- package/src/cli/qr.ts +4 -3
- package/src/cli/ui.ts +8 -4
- package/src/doctor/checks.ts +145 -2
- package/src/hostd/tailscale.ts +12 -1
- package/src/init/index.ts +35 -8
- package/src/init/run-bun-install.ts +71 -37
- package/src/inspect/transcript-view.ts +15 -2
- package/src/portbroker/hostd-client.ts +32 -6
- package/src/shared/index.ts +4 -0
- package/src/shared/platform.ts +11 -0
- package/src/shared/wsl.ts +139 -0
- package/src/tui/index.ts +26 -8
- package/src/tui/terminal-guard.ts +139 -0
- package/typeclaw.schema.json +2 -2
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
registerCommands,
|
|
49
49
|
type DiscordCommandDeclaration,
|
|
50
50
|
} from './discord-bot-slash-commands'
|
|
51
|
+
import { addDiscordMentionHints, type DiscordMentionUser } from './mention-hints'
|
|
51
52
|
|
|
52
53
|
// One declared slash command per logical agent gesture. /stop maps to the
|
|
53
54
|
// existing channel-command of the same name in the router. Adding new
|
|
@@ -507,6 +508,7 @@ type DiscordRawHistoryMessage = {
|
|
|
507
508
|
author: { id: string; username?: string; global_name?: string | null; bot?: boolean }
|
|
508
509
|
content: string
|
|
509
510
|
timestamp: string
|
|
511
|
+
mentions?: DiscordMentionUser[]
|
|
510
512
|
message_reference?: { message_id?: string; channel_id?: string }
|
|
511
513
|
attachments?: DiscordFile[]
|
|
512
514
|
embeds?: DiscordGatewayEmbed[]
|
|
@@ -597,7 +599,7 @@ function mapDiscordMessage(msg: DiscordRawHistoryMessage, botUserId: string | nu
|
|
|
597
599
|
// never resolve them. Mirror the classifier's splitInbound: bake placeholders
|
|
598
600
|
// into text and carry the structured attachments so the router can resolve ids.
|
|
599
601
|
const attachments = describeDiscordMedia(source)
|
|
600
|
-
const text = bodyOf(source)
|
|
602
|
+
const text = addDiscordMentionHints(bodyOf(source), mentionUserMap(source.mentions), { botUserId })
|
|
601
603
|
return {
|
|
602
604
|
externalMessageId: msg.id,
|
|
603
605
|
authorId: source.author.id,
|
|
@@ -617,6 +619,10 @@ function bodyOf(msg: DiscordRawHistoryMessage): string {
|
|
|
617
619
|
return msg.content === '' ? placeholders : `${msg.content}\n${placeholders}`
|
|
618
620
|
}
|
|
619
621
|
|
|
622
|
+
function mentionUserMap(mentions: readonly DiscordMentionUser[] | undefined): Map<string, DiscordMentionUser> {
|
|
623
|
+
return new Map((mentions ?? []).map((user) => [user.id, user]))
|
|
624
|
+
}
|
|
625
|
+
|
|
620
626
|
function clampLimit(requested: number, max: number): number {
|
|
621
627
|
if (!Number.isFinite(requested) || requested <= 0) return max
|
|
622
628
|
return Math.min(Math.floor(requested), max)
|
|
@@ -932,9 +938,10 @@ export function createDiscordBotAdapter(options: DiscordBotAdapterOptions): Disc
|
|
|
932
938
|
return
|
|
933
939
|
}
|
|
934
940
|
|
|
941
|
+
const hintedText = addDiscordMentionHints(verdict.payload.text, mentionUserMap(event.mentions), { botUserId })
|
|
935
942
|
const replyMessageId = event.message_reference?.message_id
|
|
936
943
|
const referenceResult = await enrichDiscordMessageReferences({
|
|
937
|
-
text:
|
|
944
|
+
text: hintedText,
|
|
938
945
|
...(replyMessageId !== undefined
|
|
939
946
|
? { reply: { channelId: event.message_reference?.channel_id ?? event.channel_id, messageId: replyMessageId } }
|
|
940
947
|
: {}),
|
|
@@ -950,8 +957,8 @@ export function createDiscordBotAdapter(options: DiscordBotAdapterOptions): Disc
|
|
|
950
957
|
})
|
|
951
958
|
const payload =
|
|
952
959
|
referenceResult.referenceContext === undefined
|
|
953
|
-
? verdict.payload
|
|
954
|
-
: { ...verdict.payload, referenceContext: referenceResult.referenceContext }
|
|
960
|
+
? { ...verdict.payload, text: hintedText }
|
|
961
|
+
: { ...verdict.payload, text: hintedText, referenceContext: referenceResult.referenceContext }
|
|
955
962
|
|
|
956
963
|
const routedTag = await formatChannelTag(payload.workspace, payload.chat)
|
|
957
964
|
logger.info(
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type DiscordMentionUser = { id: string; username?: string; global_name?: string | null }
|
|
2
|
+
|
|
3
|
+
export type MentionHintOptions = { botUserId?: string | null }
|
|
4
|
+
|
|
5
|
+
// Slack encodes user mentions as `<@U…>`/`<@W…>`, optionally with a native
|
|
6
|
+
// `|label` fallback suffix. We capture the id and the whole token so the bare
|
|
7
|
+
// `<@id>` can be reconstructed (dropping any legacy label) and a resolved hint
|
|
8
|
+
// appended after it.
|
|
9
|
+
const SLACK_MENTION_PATTERN = /<@([UW][A-Z0-9]+)(?:\|[^>]*)?>/g
|
|
10
|
+
|
|
11
|
+
// Discord uses `<@id>` and the nickname form `<@!id>`; the `!` is optional and
|
|
12
|
+
// irrelevant to the target user, so it is captured but discarded on rewrite.
|
|
13
|
+
const DISCORD_MENTION_PATTERN = /<@!?(\d+)>/g
|
|
14
|
+
|
|
15
|
+
export async function addSlackMentionHints(
|
|
16
|
+
text: string,
|
|
17
|
+
resolveUserName: (id: string) => Promise<string>,
|
|
18
|
+
options: MentionHintOptions = {},
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
const ids = new Set<string>()
|
|
21
|
+
for (const match of text.matchAll(SLACK_MENTION_PATTERN)) ids.add(match[1]!)
|
|
22
|
+
if (ids.size === 0) return text
|
|
23
|
+
|
|
24
|
+
const hints = new Map<string, string>()
|
|
25
|
+
await Promise.all(
|
|
26
|
+
Array.from(ids).map(async (id) => {
|
|
27
|
+
const hint = resolveHint(id, await resolveUserName(id), options.botUserId)
|
|
28
|
+
if (hint !== null) hints.set(id, hint)
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return text.replace(SLACK_MENTION_PATTERN, (_token, id: string) => renderToken(id, hints.get(id)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function addDiscordMentionHints(
|
|
36
|
+
text: string,
|
|
37
|
+
usersById: Map<string, DiscordMentionUser>,
|
|
38
|
+
options: MentionHintOptions = {},
|
|
39
|
+
): string {
|
|
40
|
+
return text.replace(DISCORD_MENTION_PATTERN, (token, id: string) => {
|
|
41
|
+
const user = usersById.get(id)
|
|
42
|
+
const name = user === undefined ? id : (user.global_name ?? user.username ?? id)
|
|
43
|
+
const hint = resolveHint(id, name, options.botUserId)
|
|
44
|
+
return hint === null ? token : `${token} (${hint})`
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveHint(id: string, resolvedName: string, botUserId: string | null | undefined): string | null {
|
|
49
|
+
if (id === botUserId) return 'you'
|
|
50
|
+
// The resolver echoes the id back when it cannot find a name; a bare id is
|
|
51
|
+
// not a useful hint, so leave the token unannotated in that case.
|
|
52
|
+
if (resolvedName === id || resolvedName === '') return null
|
|
53
|
+
return resolvedName
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderToken(id: string, hint: string | undefined): string {
|
|
57
|
+
return hint === undefined ? `<@${id}>` : `<@${id}> (${hint})`
|
|
58
|
+
}
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
} from '@/channels/types'
|
|
33
33
|
import { chunkMarkdown } from '@/markdown'
|
|
34
34
|
|
|
35
|
+
import { addSlackMentionHints } from './mention-hints'
|
|
35
36
|
import { createSlackAuthorResolver, type SlackAuthorResolver } from './slack-bot-author-resolver'
|
|
36
37
|
import { createSlackChannelResolver } from './slack-bot-channel-resolver'
|
|
37
38
|
import {
|
|
@@ -703,11 +704,14 @@ export function createSlackHistoryCallback(deps: {
|
|
|
703
704
|
// users.info entry. The resolver caches/coalesces, so repeated authors
|
|
704
705
|
// cost one lookup each.
|
|
705
706
|
if (authorResolver !== undefined) {
|
|
707
|
+
const resolver = authorResolver
|
|
708
|
+
const botId = botUserIdRef()
|
|
706
709
|
await Promise.all(
|
|
707
710
|
mapped.map(async (message, index) => {
|
|
711
|
+
message.text = await addSlackMentionHints(message.text, resolver.resolve, { botUserId: botId })
|
|
708
712
|
const userId = rawMessages[index]?.user
|
|
709
713
|
if (userId === undefined || userId === '') return
|
|
710
|
-
message.authorName = await
|
|
714
|
+
message.authorName = await resolver.resolve(userId)
|
|
711
715
|
}),
|
|
712
716
|
)
|
|
713
717
|
}
|
|
@@ -1133,9 +1137,10 @@ export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBot
|
|
|
1133
1137
|
}
|
|
1134
1138
|
|
|
1135
1139
|
dedupe.mark(event)
|
|
1140
|
+
const hintedText = await addSlackMentionHints(verdict.payload.text, authorResolver.resolve, { botUserId })
|
|
1136
1141
|
const slackAttachments = Array.isArray(event.attachments) ? event.attachments : undefined
|
|
1137
1142
|
const referenceResult = await enrichSlackReferenceContext({
|
|
1138
|
-
text:
|
|
1143
|
+
text: hintedText,
|
|
1139
1144
|
channelId: event.channel,
|
|
1140
1145
|
messageTs: event.ts,
|
|
1141
1146
|
...(slackAttachments !== undefined ? { attachments: slackAttachments } : {}),
|
|
@@ -1143,6 +1148,7 @@ export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBot
|
|
|
1143
1148
|
})
|
|
1144
1149
|
const enriched = {
|
|
1145
1150
|
...verdict.payload,
|
|
1151
|
+
text: hintedText,
|
|
1146
1152
|
authorName: resolvedUserName,
|
|
1147
1153
|
...(referenceResult.referenceContext !== undefined
|
|
1148
1154
|
? { referenceContext: referenceResult.referenceContext }
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
// descriptive ("I checked and it's fine") or other-directed ("you can continue")
|
|
14
14
|
// usage. This is a HINT, not a control-flow authority: the abort still fires
|
|
15
15
|
// regardless; only the optional nudge is gated on it.
|
|
16
|
+
//
|
|
17
|
+
// Detection is two-pass: a phrase-substring pass (ALL_PHRASES) for analytic
|
|
18
|
+
// languages where future intent is a separate word ("I'll", "voy a", "我会"),
|
|
19
|
+
// plus a morpheme pass (MORPHEME_PATTERNS + the Japanese check) for languages
|
|
20
|
+
// where it is a verb inflection/affix. The morpheme pass matches the marker
|
|
21
|
+
// itself, so it generalizes across EVERY action verb (update/configure/fix/…)
|
|
22
|
+
// instead of enumerating each — what the per-verb KO/TR/HI/JA lists used to do
|
|
23
|
+
// by hand.
|
|
16
24
|
|
|
17
25
|
// Strip markdown emphasis/code fences before matching so an inline `gh` span
|
|
18
26
|
// inside "바로 `gh`로 확인할게요" does not split the phrase.
|
|
@@ -62,70 +70,80 @@ const EN_PHRASES: readonly string[] = [
|
|
|
62
70
|
'lemme check',
|
|
63
71
|
'lemme look',
|
|
64
72
|
'lemme take a look',
|
|
73
|
+
// Action/config verb family. The retrieval verbs above ("check/look/look up")
|
|
74
|
+
// miss the much larger class of "I'll DO X" promises — update/configure/set up/
|
|
75
|
+
// schedule/fix/apply/create — which is exactly the class that silently truncates
|
|
76
|
+
// when the model forgets `continue: true` (the cron-update production miss).
|
|
77
|
+
"i'll update",
|
|
78
|
+
"i'll set up",
|
|
79
|
+
"i'll set it up",
|
|
80
|
+
"i'll configure",
|
|
81
|
+
"i'll schedule",
|
|
82
|
+
"i'll fix",
|
|
83
|
+
"i'll apply",
|
|
84
|
+
"i'll add",
|
|
85
|
+
"i'll create",
|
|
86
|
+
"i'll handle",
|
|
87
|
+
'let me update',
|
|
88
|
+
'let me fix',
|
|
89
|
+
'let me set up',
|
|
90
|
+
'let me configure',
|
|
91
|
+
'let me add',
|
|
92
|
+
'let me create',
|
|
93
|
+
'let me handle',
|
|
65
94
|
]
|
|
66
95
|
|
|
67
|
-
// Korean:
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
96
|
+
// Korean: the -겠습니다/-겠어요 and -ㄹ게요 verb endings are first-person
|
|
97
|
+
// volitional — they cannot address the listener, so they are safe self-direction
|
|
98
|
+
// anchors. The -겠습니다/-겠어요 form is matched by MORPHEME_PATTERNS below (it
|
|
99
|
+
// generalizes across all action verbs), so only the -게요/-게여 forms and stall
|
|
100
|
+
// idioms are enumerated here. Bare adverb+noun fragments ("바로 확인", "계속 확인",
|
|
101
|
+
// "곧 알려") are deliberately NOT listed: without the volitional ending they match
|
|
102
|
+
// other-directed requests ("바로 확인 부탁드려요" = "please check") and descriptive
|
|
103
|
+
// progressives ("계속 확인 중입니다" = "I'm still checking") — the exact false
|
|
104
|
+
// positives the design forbids. Their volitional forms are caught by the morpheme
|
|
105
|
+
// regex regardless.
|
|
72
106
|
const KO_PHRASES: readonly string[] = [
|
|
73
107
|
'확인해볼게요',
|
|
74
108
|
'확인해 볼게요',
|
|
75
109
|
'확인할게요',
|
|
76
|
-
'
|
|
77
|
-
'확인해보겠습니다',
|
|
78
|
-
'확인해 보겠습니다',
|
|
79
|
-
'다시 확인하겠습니다',
|
|
80
|
-
'다시 확인해보겠습니다',
|
|
81
|
-
'이어서 확인',
|
|
82
|
-
'계속 확인',
|
|
110
|
+
'확인할게여',
|
|
83
111
|
'계속 진행할게요',
|
|
84
|
-
'계속 진행하겠습니다',
|
|
85
|
-
'계속하겠습니다',
|
|
86
112
|
'계속할게요',
|
|
87
|
-
'바로 확인',
|
|
88
113
|
'바로 볼게요',
|
|
89
|
-
'바로 진행',
|
|
90
114
|
'살펴볼게요',
|
|
91
|
-
'살펴보겠습니다',
|
|
92
|
-
'진행하겠습니다',
|
|
93
|
-
'잠시만요',
|
|
94
|
-
'잠깐만요',
|
|
95
|
-
'곧 알려',
|
|
96
|
-
// Bare first-person-volitional verb endings: the -ㄹ게요/-겠습니다 ending is
|
|
97
|
-
// self-directed regardless of the preceding adverb, so the "바로 …" prefix in
|
|
98
|
-
// the entries above is not load-bearing. "볼게요" alone (and "먼저/한번/지금 볼게요"
|
|
99
|
-
// by substring) is the exact production miss — the ack "…먼저 볼게요" did not
|
|
100
|
-
// match because only the "바로 볼게요" compound was listed. Common work verbs
|
|
101
|
-
// (검토/조회/찾아/알아/처리) in the same volitional form join here for parity with
|
|
102
|
-
// "확인/살펴" above; "볼게여" is the casual -여 variant seen in chat.
|
|
103
115
|
'볼게요',
|
|
104
116
|
'볼게여',
|
|
105
|
-
'확인할게여',
|
|
106
117
|
'검토할게요',
|
|
107
118
|
'검토해볼게요',
|
|
108
|
-
'검토하겠습니다',
|
|
109
119
|
'조회해볼게요',
|
|
110
|
-
'조회하겠습니다',
|
|
111
120
|
'찾아볼게요',
|
|
112
|
-
'찾아보겠습니다',
|
|
113
121
|
'알아볼게요',
|
|
114
|
-
'알아보겠습니다',
|
|
115
122
|
'처리할게요',
|
|
116
|
-
'
|
|
123
|
+
'알려드릴게요',
|
|
124
|
+
// Action/config verb -게요 forms (the -겠습니다 siblings are covered by the
|
|
125
|
+
// morpheme regex; these are the casual-polite variants chat models also emit).
|
|
126
|
+
'업데이트할게요',
|
|
127
|
+
'수정할게요',
|
|
128
|
+
'설정할게요',
|
|
129
|
+
'반영할게요',
|
|
130
|
+
'적용할게요',
|
|
131
|
+
'추가할게요',
|
|
132
|
+
'생성할게요',
|
|
133
|
+
'잠시만요',
|
|
134
|
+
'잠깐만요',
|
|
117
135
|
]
|
|
118
136
|
|
|
119
137
|
// The remaining languages mirror the precision-first selection above: every
|
|
120
138
|
// entry pairs a FIRST-PERSON future/volitional anchor with a work verb
|
|
121
|
-
// (check/look/continue/proceed/verify) or is an
|
|
122
|
-
// now"). The same false-negative bias holds — bare
|
|
123
|
-
// ("ok", "sí", "好"), second-person imperatives ("you
|
|
124
|
-
// descriptive past forms ("I checked") are deliberately excluded
|
|
125
|
-
// substring match on those would mis-fire. Latin/Cyrillic/Arabic/Indic
|
|
126
|
-
// are inflected first-person-future forms (or multi-word) so they cannot
|
|
127
|
-
// collide with a bare common word; CJK entries are full 4+ character
|
|
128
|
-
//
|
|
139
|
+
// (check/look/continue/proceed/verify or update/configure/fix/create) or is an
|
|
140
|
+
// immediate-work idiom ("on it now"). The same false-negative bias holds — bare
|
|
141
|
+
// verbs, bare acknowledgments ("ok", "sí", "好"), second-person imperatives ("you
|
|
142
|
+
// continue"), and descriptive past forms ("I checked") are deliberately excluded
|
|
143
|
+
// because a substring match on those would mis-fire. Latin/Cyrillic/Arabic/Indic
|
|
144
|
+
// entries are inflected first-person-future forms (or multi-word) so they cannot
|
|
145
|
+
// collide with a bare common word; CJK entries are full 4+ character intent
|
|
146
|
+
// phrases, never a lone noun.
|
|
129
147
|
|
|
130
148
|
// Spanish: "voy a" / "déjame" + work verb; "enseguida" (right away) idioms.
|
|
131
149
|
const ES_PHRASES: readonly string[] = [
|
|
@@ -152,6 +170,17 @@ const ES_PHRASES: readonly string[] = [
|
|
|
152
170
|
'un momento',
|
|
153
171
|
'dame un momento',
|
|
154
172
|
'dame un segundo',
|
|
173
|
+
// Action/config verb family.
|
|
174
|
+
'voy a actualizar',
|
|
175
|
+
'voy a configurar',
|
|
176
|
+
'voy a corregir',
|
|
177
|
+
'voy a arreglar',
|
|
178
|
+
'voy a crear',
|
|
179
|
+
'voy a añadir',
|
|
180
|
+
'voy a programar',
|
|
181
|
+
'voy a aplicar',
|
|
182
|
+
'déjame actualizar',
|
|
183
|
+
'déjame corregir',
|
|
155
184
|
]
|
|
156
185
|
|
|
157
186
|
// French: "je vais" + work verb; "laisse-moi" idioms.
|
|
@@ -174,6 +203,16 @@ const FR_PHRASES: readonly string[] = [
|
|
|
174
203
|
'un instant',
|
|
175
204
|
'donne-moi un instant',
|
|
176
205
|
'donne-moi une seconde',
|
|
206
|
+
// Action/config verb family.
|
|
207
|
+
'je vais mettre à jour',
|
|
208
|
+
'je vais configurer',
|
|
209
|
+
'je vais corriger',
|
|
210
|
+
'je vais créer',
|
|
211
|
+
'je vais ajouter',
|
|
212
|
+
'je vais programmer',
|
|
213
|
+
'je vais appliquer',
|
|
214
|
+
'laisse-moi corriger',
|
|
215
|
+
'laisse-moi mettre à jour',
|
|
177
216
|
]
|
|
178
217
|
|
|
179
218
|
// Italian: "vado a" / "fammi" + work verb; "controllo subito" idioms.
|
|
@@ -194,6 +233,15 @@ const IT_PHRASES: readonly string[] = [
|
|
|
194
233
|
'un momento',
|
|
195
234
|
'dammi un momento',
|
|
196
235
|
'dammi un secondo',
|
|
236
|
+
// Action/config verb family.
|
|
237
|
+
'vado ad aggiornare',
|
|
238
|
+
'vado a configurare',
|
|
239
|
+
'vado a correggere',
|
|
240
|
+
'vado a creare',
|
|
241
|
+
'vado ad aggiungere',
|
|
242
|
+
'vado ad applicare',
|
|
243
|
+
'fammi aggiornare',
|
|
244
|
+
'fammi correggere',
|
|
197
245
|
]
|
|
198
246
|
|
|
199
247
|
// Portuguese: "vou" + work verb; "deixa eu" idioms.
|
|
@@ -214,6 +262,16 @@ const PT_PHRASES: readonly string[] = [
|
|
|
214
262
|
'um momento',
|
|
215
263
|
'me dê um momento',
|
|
216
264
|
'me dá um segundo',
|
|
265
|
+
// Action/config verb family.
|
|
266
|
+
'vou atualizar',
|
|
267
|
+
'vou configurar',
|
|
268
|
+
'vou corrigir',
|
|
269
|
+
'vou criar',
|
|
270
|
+
'vou adicionar',
|
|
271
|
+
'vou agendar',
|
|
272
|
+
'vou aplicar',
|
|
273
|
+
'deixa eu atualizar',
|
|
274
|
+
'deixa eu corrigir',
|
|
217
275
|
]
|
|
218
276
|
|
|
219
277
|
// German: "ich werde" / "lass mich" + work verb; "ich schaue gleich" idioms.
|
|
@@ -238,10 +296,22 @@ const DE_PHRASES: readonly string[] = [
|
|
|
238
296
|
'einen moment',
|
|
239
297
|
'einen augenblick',
|
|
240
298
|
'gib mir eine sekunde',
|
|
299
|
+
// Action/config verb family.
|
|
300
|
+
'ich werde aktualisieren',
|
|
301
|
+
'ich werde konfigurieren',
|
|
302
|
+
'ich werde korrigieren',
|
|
303
|
+
'ich werde einrichten',
|
|
304
|
+
'ich werde erstellen',
|
|
305
|
+
'ich werde hinzufügen',
|
|
306
|
+
'ich werde anwenden',
|
|
307
|
+
'lass mich aktualisieren',
|
|
308
|
+
'lass mich korrigieren',
|
|
241
309
|
]
|
|
242
310
|
|
|
243
311
|
// Russian: first-person-future verbs (проверю/посмотрю/продолжу) — the -ю/-у
|
|
244
|
-
// inflection is unambiguously "I will", so it is a safe self-anchor.
|
|
312
|
+
// inflection is unambiguously "I will", so it is a safe self-anchor. (Note: the
|
|
313
|
+
// bare -ю ending is shared with present-imperfective "я делаю" = "I do", so this
|
|
314
|
+
// stays an enumerated list rather than a morpheme regex.)
|
|
245
315
|
const RU_PHRASES: readonly string[] = [
|
|
246
316
|
'сейчас проверю',
|
|
247
317
|
'я проверю',
|
|
@@ -254,6 +324,15 @@ const RU_PHRASES: readonly string[] = [
|
|
|
254
324
|
'дайте мне минуту',
|
|
255
325
|
'одну секунду',
|
|
256
326
|
'минутку',
|
|
327
|
+
// Action/config verb family (perfective first-person futures).
|
|
328
|
+
'я обновлю',
|
|
329
|
+
'я настрою',
|
|
330
|
+
'я исправлю',
|
|
331
|
+
'я создам',
|
|
332
|
+
'я добавлю',
|
|
333
|
+
'я применю',
|
|
334
|
+
'сейчас обновлю',
|
|
335
|
+
'сейчас исправлю',
|
|
257
336
|
]
|
|
258
337
|
|
|
259
338
|
// Chinese: 我会/我来/我再 + work verb. Full multi-character intent phrases only;
|
|
@@ -277,26 +356,39 @@ const ZH_PHRASES: readonly string[] = [
|
|
|
277
356
|
'让我检查一下',
|
|
278
357
|
'稍等一下',
|
|
279
358
|
'我看一下',
|
|
359
|
+
// Action/config verb family.
|
|
360
|
+
'我来更新',
|
|
361
|
+
'我会更新',
|
|
362
|
+
'我来配置',
|
|
363
|
+
'我来设置',
|
|
364
|
+
'我会设置',
|
|
365
|
+
'我来修改',
|
|
366
|
+
'我会修改',
|
|
367
|
+
'我来修复',
|
|
368
|
+
'我来创建',
|
|
369
|
+
'我来添加',
|
|
370
|
+
'我马上更新',
|
|
371
|
+
'让我更新',
|
|
372
|
+
'让我改一下',
|
|
280
373
|
]
|
|
281
374
|
|
|
282
|
-
// Japanese:
|
|
283
|
-
//
|
|
375
|
+
// Japanese: handled by the JA_VOLITIONAL morpheme check below (-します/-いたします/
|
|
376
|
+
// -してみます generalizes across all する action verbs). Only the regular-verb
|
|
377
|
+
// ます forms (調べます/見てみます/続けます — bare ます is too broad to regex) and
|
|
378
|
+
// stall idioms are enumerated here.
|
|
284
379
|
const JA_PHRASES: readonly string[] = [
|
|
285
|
-
'確認します',
|
|
286
|
-
'確認してみます',
|
|
287
|
-
'確認いたします',
|
|
288
380
|
'調べてみます',
|
|
289
381
|
'調べます',
|
|
290
382
|
'見てみます',
|
|
291
383
|
'続けます',
|
|
292
|
-
'引き続き確認します',
|
|
293
|
-
'すぐ確認します',
|
|
294
384
|
'少々お待ちください',
|
|
295
385
|
'ちょっと待ってください',
|
|
296
386
|
]
|
|
297
387
|
|
|
298
388
|
// Arabic: future particle سـ prefixed first-person verb (سأتحقق = "I will
|
|
299
|
-
// verify"). The سأ prefix is
|
|
389
|
+
// verify"). The سأ prefix is first-person-future, but as a bare substring it
|
|
390
|
+
// collides with the root س-أ-ل ("ask": سألت "I asked", المسألة "the matter"), so
|
|
391
|
+
// this stays an enumerated list of full verbs rather than a سأ-prefix regex.
|
|
300
392
|
const AR_PHRASES: readonly string[] = [
|
|
301
393
|
'سأتحقق',
|
|
302
394
|
'سأتأكد',
|
|
@@ -307,30 +399,31 @@ const AR_PHRASES: readonly string[] = [
|
|
|
307
399
|
'دعني أتحقق',
|
|
308
400
|
'دعني أراجع',
|
|
309
401
|
'لحظة من فضلك',
|
|
402
|
+
// Action/config verb family.
|
|
403
|
+
'سأحدث',
|
|
404
|
+
'سأحدّث',
|
|
405
|
+
'سأعدل',
|
|
406
|
+
'سأعدّل',
|
|
407
|
+
'سأضبط',
|
|
408
|
+
'سأصلح',
|
|
409
|
+
'سأنشئ',
|
|
410
|
+
'سأضيف',
|
|
310
411
|
]
|
|
311
412
|
|
|
312
|
-
// Hindi: first-person-future
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
'जांच करूंगा',
|
|
317
|
-
'देख लूँगा',
|
|
318
|
-
'देख लूंगा',
|
|
319
|
-
'जारी रखूँगा',
|
|
320
|
-
'जारी रखूंगा',
|
|
321
|
-
'एक मिनट रुकिए',
|
|
322
|
-
]
|
|
413
|
+
// Hindi: first-person-future is the -ūṅgā/-ūṅgī suffix, matched by
|
|
414
|
+
// MORPHEME_PATTERNS below (it covers all X-करना compounds). Only the stall idiom
|
|
415
|
+
// is enumerated here.
|
|
416
|
+
const HI_PHRASES: readonly string[] = ['एक मिनट रुकिए']
|
|
323
417
|
|
|
324
|
-
// Turkish: first-person-future "-eceğim/-acağım"
|
|
418
|
+
// Turkish: first-person-future "-eceğim/-acağım" is matched by MORPHEME_PATTERNS
|
|
419
|
+
// below. The present-progressive ("ediyorum" = "I'm checking now"), optative
|
|
420
|
+
// ("bir bakayım" = "let me look"), and stall idioms stay enumerated — the
|
|
421
|
+
// progressive -ıyorum ending is too polysemous to regex ("biliyorum" = "I know").
|
|
325
422
|
const TR_PHRASES: readonly string[] = [
|
|
326
|
-
'kontrol edeceğim',
|
|
327
423
|
'kontrol ediyorum',
|
|
328
|
-
'bakacağım',
|
|
329
424
|
'bir bakayım',
|
|
330
425
|
'bir kontrol edeyim',
|
|
331
426
|
'kontrol edeyim',
|
|
332
|
-
'inceleyeceğim',
|
|
333
|
-
'devam edeceğim',
|
|
334
427
|
'hemen kontrol ediyorum',
|
|
335
428
|
'hemen bakıyorum',
|
|
336
429
|
'bir saniye',
|
|
@@ -348,6 +441,14 @@ const VI_PHRASES: readonly string[] = [
|
|
|
348
441
|
'tôi xem ngay',
|
|
349
442
|
'chờ một chút',
|
|
350
443
|
'đợi một chút',
|
|
444
|
+
// Action/config verb family.
|
|
445
|
+
'tôi sẽ cập nhật',
|
|
446
|
+
'tôi sẽ cấu hình',
|
|
447
|
+
'tôi sẽ sửa',
|
|
448
|
+
'tôi sẽ tạo',
|
|
449
|
+
'tôi sẽ thêm',
|
|
450
|
+
'để tôi cập nhật',
|
|
451
|
+
'để tôi sửa',
|
|
351
452
|
]
|
|
352
453
|
|
|
353
454
|
// Indonesian: "saya akan" / "biar saya" (I will / let me) + work verb.
|
|
@@ -362,6 +463,16 @@ const ID_PHRASES: readonly string[] = [
|
|
|
362
463
|
'saya periksa dulu',
|
|
363
464
|
'tunggu sebentar',
|
|
364
465
|
'sebentar ya',
|
|
466
|
+
// Action/config verb family.
|
|
467
|
+
'saya akan perbarui',
|
|
468
|
+
'saya akan memperbarui',
|
|
469
|
+
'saya akan atur',
|
|
470
|
+
'saya akan konfigurasi',
|
|
471
|
+
'saya akan perbaiki',
|
|
472
|
+
'saya akan buat',
|
|
473
|
+
'saya akan tambah',
|
|
474
|
+
'biar saya perbaiki',
|
|
475
|
+
'biar saya perbarui',
|
|
365
476
|
]
|
|
366
477
|
|
|
367
478
|
const ALL_PHRASES: readonly string[] = [
|
|
@@ -382,6 +493,41 @@ const ALL_PHRASES: readonly string[] = [
|
|
|
382
493
|
...ID_PHRASES,
|
|
383
494
|
]
|
|
384
495
|
|
|
496
|
+
// First-person future/volitional realized as a verb inflection or affix (not a
|
|
497
|
+
// separate word), so matching the marker generalizes across ALL action verbs.
|
|
498
|
+
const MORPHEME_PATTERNS: readonly RegExp[] = [
|
|
499
|
+
// Korean first-person volitional: a verb stem + -겠습니다/-겠어요. The stem must
|
|
500
|
+
// be one of the action/auxiliary verbs 하 (the 하다 light verb behind every
|
|
501
|
+
// X하다 — 업데이트하겠습니다/반영하겠어요), 보 (살펴보겠습니다), 두 (반영해두겠습니다), or
|
|
502
|
+
// 놓 (해놓겠습니다). Anchoring on the verb stem is what keeps this self-directed:
|
|
503
|
+
// bare 겠 also matches adjective-stem CONJECTURE (좋겠어요 "that'd be nice",
|
|
504
|
+
// 괜찮겠어요 "must be fine") and the idioms 알겠/모르겠, none of which promise work.
|
|
505
|
+
// Listener-directed conjecture takes the honorific 시 (피곤하시겠어요 → 시겠, not
|
|
506
|
+
// 하겠), so it is excluded too.
|
|
507
|
+
/(?:하|보|두|놓)겠(?:습니다|어요)/,
|
|
508
|
+
// Turkish first-person-singular future "-acağım/-eceğim" ("I will VERB").
|
|
509
|
+
// Vowel harmony yields exactly these two suffixes; the y-buffer
|
|
510
|
+
// ("bekleyeceğim") leaves the suffix intact.
|
|
511
|
+
/acağım|eceğim/,
|
|
512
|
+
// Hindi first-person future "-ūṅgā/-ūṅgī" on any verb, covering all X-करना
|
|
513
|
+
// compounds ("अपडेट करूँगा"). Both nasal spellings (ँ U+0901 / ं U+0902) and
|
|
514
|
+
// both genders (ा/ी) are included.
|
|
515
|
+
/ू[ँं]ग[ाी]/,
|
|
516
|
+
]
|
|
517
|
+
|
|
518
|
+
// Japanese する-verb volitional します/いたします/してみます — covers the action class
|
|
519
|
+
// (更新します/設定します/対応します). Bare ます is the universal polite verb ending and
|
|
520
|
+
// far too broad to match, so this keys on します specifically. Two request/greeting
|
|
521
|
+
// idioms end in します without being work intent — お願い(いた)します ("please") and
|
|
522
|
+
// 失礼します ("excuse me") — and are stripped before the test so they don't fire.
|
|
523
|
+
// The (?![か??]) lookahead drops question forms — both the か question particle
|
|
524
|
+
// (どうしますか "what should I do?") and a trailing question mark, fullwidth ? or
|
|
525
|
+
// ASCII ? (どうします?, 更新します? "shall I update?"). A question awaits the user;
|
|
526
|
+
// it is not a commitment to act this turn. A statement keeps its 。/, so
|
|
527
|
+
// 更新します。 still matches.
|
|
528
|
+
const JA_VOLITIONAL_IDIOMS = /お願い(?:いた)?します|失礼します/g
|
|
529
|
+
const JA_VOLITIONAL = /します(?![か??])|してみます(?![か??])/
|
|
530
|
+
|
|
385
531
|
// Reply texts shorter than this are almost always a complete final answer
|
|
386
532
|
// ("네", "ok", "done") where a partial match would be noise. The shortest
|
|
387
533
|
// legitimate intent phrases ("on it now", "확인할게요") clear this floor.
|
|
@@ -391,5 +537,7 @@ export function detectContinuationWillingness(text: string): boolean {
|
|
|
391
537
|
if (text.length < MIN_LENGTH) return false
|
|
392
538
|
const normalized = normalize(text)
|
|
393
539
|
if (normalized.length < MIN_LENGTH) return false
|
|
394
|
-
|
|
540
|
+
if (ALL_PHRASES.some((phrase) => normalized.includes(phrase))) return true
|
|
541
|
+
if (MORPHEME_PATTERNS.some((pattern) => pattern.test(normalized))) return true
|
|
542
|
+
return JA_VOLITIONAL.test(normalized.replace(JA_VOLITIONAL_IDIOMS, ''))
|
|
395
543
|
}
|
package/src/channels/router.ts
CHANGED
|
@@ -97,6 +97,11 @@ export const MAX_DEBOUNCE_MS = 4000
|
|
|
97
97
|
export const HOT_THRESHOLD_MS = 3000
|
|
98
98
|
export const MAX_CONSECUTIVE_ABORTS = 3
|
|
99
99
|
export const CONTEXT_BUFFER_SIZE = 20
|
|
100
|
+
// Observed ("Recent context") messages are awareness-only and replayed in full
|
|
101
|
+
// on every turn (uncached), so one long paste would otherwise re-bloat every
|
|
102
|
+
// subsequent turn until it ages out. Cap each observed message's text; the
|
|
103
|
+
// addressed current message is never capped (it's the actual request).
|
|
104
|
+
export const OBSERVED_MESSAGE_MAX_CHARS = 800
|
|
100
105
|
// Discord's typing indicator expires after ~10s; an 8s heartbeat keeps it
|
|
101
106
|
// continuously visible while we debounce + generate without spamming the API.
|
|
102
107
|
export const TYPING_HEARTBEAT_MS = 8000
|
|
@@ -4462,7 +4467,7 @@ function composeTurnPrompt(
|
|
|
4462
4467
|
if (observed.length > 0) {
|
|
4463
4468
|
parts.push('## Recent context (not addressed to you, for awareness only)')
|
|
4464
4469
|
for (const o of observed) {
|
|
4465
|
-
parts.push(formatInboundPromptLines(o, adapter))
|
|
4470
|
+
parts.push(formatInboundPromptLines(o, adapter, OBSERVED_MESSAGE_MAX_CHARS))
|
|
4466
4471
|
}
|
|
4467
4472
|
parts.push('')
|
|
4468
4473
|
}
|
|
@@ -4521,10 +4526,22 @@ function formatAuthorLine(
|
|
|
4521
4526
|
authorName: string,
|
|
4522
4527
|
authorIsBot: boolean,
|
|
4523
4528
|
text: string,
|
|
4529
|
+
maxChars?: number,
|
|
4524
4530
|
): string {
|
|
4525
4531
|
const tag = authorIsBot ? ' [bot]' : ''
|
|
4526
4532
|
const stamp = ts > 0 ? `[${new Date(ts).toISOString()}] ` : ''
|
|
4527
|
-
return `${stamp}${formatAuthorReference(adapter, authorId, authorName)} (${authorName})${tag}: ${text}`
|
|
4533
|
+
return `${stamp}${formatAuthorReference(adapter, authorId, authorName)} (${authorName})${tag}: ${capObservedText(text, maxChars)}`
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
// Cap by whole code points so truncation never splits a surrogate pair (emoji,
|
|
4537
|
+
// astral-plane chars) into a dangling half. `text.length` (UTF-16 code units) is
|
|
4538
|
+
// a cheap upper bound on the code-point count, so a string already within the
|
|
4539
|
+
// cap skips the array build.
|
|
4540
|
+
function capObservedText(text: string, maxChars: number | undefined): string {
|
|
4541
|
+
if (maxChars === undefined || text.length <= maxChars) return text
|
|
4542
|
+
const points = Array.from(text)
|
|
4543
|
+
if (points.length <= maxChars) return text
|
|
4544
|
+
return `${points.slice(0, maxChars).join('')} […truncated]`
|
|
4528
4545
|
}
|
|
4529
4546
|
|
|
4530
4547
|
function formatInboundPromptLines(
|
|
@@ -4537,10 +4554,19 @@ function formatInboundPromptLines(
|
|
|
4537
4554
|
referenceContext?: InboundReferenceContext
|
|
4538
4555
|
},
|
|
4539
4556
|
adapter: AdapterId,
|
|
4557
|
+
maxTextChars?: number,
|
|
4540
4558
|
): string {
|
|
4541
4559
|
const lines = inbound.referenceContext?.sources.map(renderQuoteAnchor) ?? []
|
|
4542
4560
|
lines.push(
|
|
4543
|
-
formatAuthorLine(
|
|
4561
|
+
formatAuthorLine(
|
|
4562
|
+
inbound.ts,
|
|
4563
|
+
adapter,
|
|
4564
|
+
inbound.authorId,
|
|
4565
|
+
inbound.authorName,
|
|
4566
|
+
inbound.authorIsBot,
|
|
4567
|
+
inbound.text,
|
|
4568
|
+
maxTextChars,
|
|
4569
|
+
),
|
|
4544
4570
|
)
|
|
4545
4571
|
return lines.join('\n')
|
|
4546
4572
|
}
|