typeclaw 0.37.2 → 0.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # TypeClaw
2
2
 
3
3
  <p align="center">
4
- <img src="./docs/public/typeclaw.png" alt="TypeClaw logo" width="240" />
4
+ <img src="./docs/public/typeclaw-transparent.png" alt="TypeClaw logo" width="240" />
5
5
  </p>
6
6
 
7
- > The agent for perfectionists — crafted in every detail. It behaves in your team's chat and gets sharper the longer it runs. Sandboxed and self-managing.
7
+ <h3 align="center">The agent for perfectionists</h3>
8
+ <p align="center">Crafted in every detail – it behaves in your team's chat and<br />gets sharper the longer it runs. Sandboxed and self-managing.</p>
8
9
 
9
10
  ## Why?
10
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeclaw",
3
- "version": "0.37.2",
3
+ "version": "0.37.3",
4
4
  "homepage": "https://github.com/typeclaw/typeclaw#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/typeclaw/typeclaw/issues"
@@ -48,8 +48,20 @@ const EN_PHRASES: readonly string[] = [
48
48
  'looking into it now',
49
49
  'working on it now',
50
50
  'on it now',
51
+ "i'm on it",
51
52
  'give me a moment',
52
53
  'give me a sec',
54
+ // Parity additions for common first-person-future acks: "investigate / look up
55
+ // / pull up" are work-verb siblings of the "look into / dig in" entries above,
56
+ // and "lemme" is the contracted "let me" that chat models routinely emit.
57
+ "i'll investigate",
58
+ "i'll look it up",
59
+ "i'll pull that up",
60
+ "i'll pull it up",
61
+ 'let me pull',
62
+ 'lemme check',
63
+ 'lemme look',
64
+ 'lemme take a look',
53
65
  ]
54
66
 
55
67
  // Korean: -ㄹ게요 / -겠습니다 future-volitional endings on check/look/continue/
@@ -81,6 +93,27 @@ const KO_PHRASES: readonly string[] = [
81
93
  '잠시만요',
82
94
  '잠깐만요',
83
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
+ '볼게요',
104
+ '볼게여',
105
+ '확인할게여',
106
+ '검토할게요',
107
+ '검토해볼게요',
108
+ '검토하겠습니다',
109
+ '조회해볼게요',
110
+ '조회하겠습니다',
111
+ '찾아볼게요',
112
+ '찾아보겠습니다',
113
+ '알아볼게요',
114
+ '알아보겠습니다',
115
+ '처리할게요',
116
+ '처리하겠습니다',
84
117
  ]
85
118
 
86
119
  // The remaining languages mirror the precision-first selection above: every
@@ -106,6 +139,12 @@ const ES_PHRASES: readonly string[] = [
106
139
  'déjame comprobar',
107
140
  'déjame verificar',
108
141
  'déjame mirar',
142
+ 'voy a echar un vistazo',
143
+ 'déjame echar un vistazo',
144
+ 'ahora lo reviso',
145
+ 'ahora reviso',
146
+ 'ahora lo verifico',
147
+ 'ahora mismo lo reviso',
109
148
  'lo reviso enseguida',
110
149
  'lo verifico enseguida',
111
150
  'enseguida lo reviso',
@@ -123,10 +162,15 @@ const FR_PHRASES: readonly string[] = [
123
162
  'je vais poursuivre',
124
163
  'je vais voir',
125
164
  'je vais contrôler',
165
+ 'je vais creuser',
166
+ 'je vais jeter un œil',
126
167
  'laisse-moi vérifier',
127
168
  'laisse-moi regarder',
169
+ 'laisse-moi jeter un œil',
128
170
  'je vérifie tout de suite',
129
171
  'je regarde tout de suite',
172
+ 'je regarde ça tout de suite',
173
+ 'je regarde ça',
130
174
  'un instant',
131
175
  'donne-moi un instant',
132
176
  'donne-moi une seconde',
@@ -137,12 +181,16 @@ const IT_PHRASES: readonly string[] = [
137
181
  'vado a controllare',
138
182
  'vado a verificare',
139
183
  'vado a guardare',
184
+ "vado a dare un'occhiata",
140
185
  'fammi controllare',
141
186
  'fammi verificare',
142
187
  'fammi guardare',
188
+ "fammi dare un'occhiata",
189
+ "do un'occhiata",
143
190
  'controllo subito',
144
191
  'verifico subito',
145
192
  'continuo subito',
193
+ 'guardo subito',
146
194
  'un momento',
147
195
  'dammi un momento',
148
196
  'dammi un secondo',
@@ -156,9 +204,11 @@ const PT_PHRASES: readonly string[] = [
156
204
  'vou olhar',
157
205
  'vou continuar',
158
206
  'vou prosseguir',
207
+ 'vou dar uma olhada',
159
208
  'deixa eu verificar',
160
209
  'deixa eu conferir',
161
210
  'deixa eu olhar',
211
+ 'deixa eu dar uma olhada',
162
212
  'verifico já',
163
213
  'já verifico',
164
214
  'um momento',
@@ -175,8 +225,13 @@ const DE_PHRASES: readonly string[] = [
175
225
  'ich werde fortfahren',
176
226
  'lass mich prüfen',
177
227
  'lass mich nachsehen',
228
+ 'lass mich schauen',
178
229
  'ich schaue gleich',
230
+ 'ich schaue mir das an',
231
+ 'ich schaue mir das mal an',
179
232
  'ich prüfe gleich',
233
+ 'ich prüfe das gleich',
234
+ 'ich sehe gleich nach',
180
235
  'gleich prüfen',
181
236
  'gleich überprüfen',
182
237
  'gleich nachsehen',
@@ -194,6 +249,8 @@ const RU_PHRASES: readonly string[] = [
194
249
  'я продолжу',
195
250
  'продолжу проверку',
196
251
  'сейчас посмотрю',
252
+ 'дай мне проверить',
253
+ 'дайте мне проверить',
197
254
  'дайте мне минуту',
198
255
  'одну секунду',
199
256
  'минутку',
@@ -214,6 +271,10 @@ const ZH_PHRASES: readonly string[] = [
214
271
  '我马上确认',
215
272
  '我马上检查',
216
273
  '我马上看',
274
+ '让我看看',
275
+ '让我查一下',
276
+ '让我确认一下',
277
+ '让我检查一下',
217
278
  '稍等一下',
218
279
  '我看一下',
219
280
  ]
@@ -265,6 +326,9 @@ const TR_PHRASES: readonly string[] = [
265
326
  'kontrol edeceğim',
266
327
  'kontrol ediyorum',
267
328
  'bakacağım',
329
+ 'bir bakayım',
330
+ 'bir kontrol edeyim',
331
+ 'kontrol edeyim',
268
332
  'inceleyeceğim',
269
333
  'devam edeceğim',
270
334
  'hemen kontrol ediyorum',
@@ -279,6 +279,27 @@ export const WILLINGNESS_NUDGE = [
279
279
  '',
280
280
  '---',
281
281
  ].join('\n')
282
+ // Injected when a `channel_send` ack tripped continuation-willingness, the model
283
+ // did fresh work after it, then ended on an EMPTY `stop` leaf — the answer was
284
+ // computed but never sent (the Kimi/Fireworks empty-completion flake). Distinct
285
+ // from WILLINGNESS_NUDGE: that path is a `channel_reply` that ended the turn and
286
+ // needs `continue: true`; this path is a `channel_send` (which never ends the
287
+ // turn) whose follow-up degenerated, so the model just needs to emit the reply it
288
+ // already worked out. Shares MAX_WILLINGNESS_NUDGES so a turn can't double-nudge.
289
+ export const SEND_WILLINGNESS_NUDGE = [
290
+ '---',
291
+ '**[SYSTEM MESSAGE — not from a human]**',
292
+ '',
293
+ 'You said you would keep working this turn and did the work, but the turn ended',
294
+ 'without sending the result — nothing reached the channel after your last',
295
+ 'message. This is an automated signal from the channel router, not a message',
296
+ 'from anyone in the chat. **Do not acknowledge or reply to this notice itself.**',
297
+ '',
298
+ 'Send the answer you just worked out now via your channel send tool. If you',
299
+ 'genuinely have nothing to report, reply with `NO_REPLY`.',
300
+ '',
301
+ '---',
302
+ ].join('\n')
282
303
  // Rolling window for outbound send-rate telemetry. 5s matches Discord's
283
304
  // rate-limit shape (5 msg / 5 s / channel) and comfortably covers Slack's
284
305
  // 1 msg/s sustained. The window is observational; exceeding the burst
@@ -3387,6 +3408,43 @@ export function createChannelRouter(options: CreateChannelRouterOptions): Channe
3387
3408
  // landed — suppress it, as before.
3388
3409
  if (live.successfulChannelSends > successfulSendsBeforePrompt) {
3389
3410
  maybeNudgeContinuationWillingness(live)
3411
+
3412
+ // A `channel_send` ack that promised to keep working, fresh post-ack work,
3413
+ // then an EMPTY `stop` leaf: the model computed the answer in its reasoning
3414
+ // / tool results but never sent it (the Kimi/Fireworks empty-completion
3415
+ // flake). `maybeNudgeContinuationWillingness` above can't catch this — it
3416
+ // reads `lastTerminalReplyAbort`, which only a `channel_reply` sets;
3417
+ // `channel_send` keeps the turn alive and stamps nothing. And the
3418
+ // stranded-toolUse retry below requires `source !== 'leaf'`, but an empty
3419
+ // `stop` leaf recovers as `source: 'leaf'`, so this shape would otherwise
3420
+ // fall straight through to the `endsWithNoReplySignal('')` → `no_reply`
3421
+ // classification. Discriminator (all on existing state, zero false positives
3422
+ // measured across the session corpus): a send landed AND the just-sent text
3423
+ // trips the precision-tuned willingness detector AND the turn-end leaf is a
3424
+ // FRESH empty `stop` (different entry than the ack's leaf — so the model did
3425
+ // post-ack work, not an ack-then-await-user stop). Bounded by
3426
+ // MAX_WILLINGNESS_NUDGES (shared with the reply path); on exhaustion post the
3427
+ // fallback rather than going silent, mirroring the stranded-toolUse path.
3428
+ // Gated on an empty `promptQueue` (like maybeNudgeContinuationWillingness): a
3429
+ // real inbound that coalesced into the just-finished prompt will be answered
3430
+ // by the next drain pass, and drain() splices pending reminders into that
3431
+ // batch — so injecting a stale recovery nudge would prepend it to a live user
3432
+ // message. Skip the nudge AND the fallback in that case and let the trailing
3433
+ // recovery below run; the queued inbound supersedes this turn's silence.
3434
+ if (live.promptQueue.length === 0 && live.currentTurnAuthorId !== null && isEmptyStopAfterWillingnessAck(live)) {
3435
+ if (live.willingnessNudges < MAX_WILLINGNESS_NUDGES) {
3436
+ live.willingnessNudges++
3437
+ logger.warn(
3438
+ `[channels] ${live.keyId} send_willingness_nudge attempt=${live.willingnessNudges}/${MAX_WILLINGNESS_NUDGES} ` +
3439
+ `cause=empty_stop_after_send_ack`,
3440
+ )
3441
+ live.pendingSystemReminders.push(SEND_WILLINGNESS_NUDGE)
3442
+ } else {
3443
+ await postEmptyTurnFallback('empty_stop_after_send_ack_nudges_exhausted')
3444
+ }
3445
+ return
3446
+ }
3447
+
3390
3448
  const trailing = recoverableAssistantText(live.session)
3391
3449
  if (trailing === null || trailing.source !== 'leaf') {
3392
3450
  // A `continue: true` status reply landed, then the turn stranded on an
@@ -5021,6 +5079,24 @@ function leafIsStrandedToolUse(session: AgentSession): boolean {
5021
5079
  return false
5022
5080
  }
5023
5081
 
5082
+ // True when the turn-end leaf is a FRESH empty `stop` (no text, no tool call,
5083
+ // distinct from the leaf in place at the last successful send) AND the most
5084
+ // recent send to this target was a continuation-willingness ack. This is the
5085
+ // `channel_send` analogue of the `channel_reply` willingness path: the model
5086
+ // acked "I'll check…", did post-ack work, then the follow-up came back as a
5087
+ // clean empty completion that would otherwise be read as a deliberate `NO_REPLY`.
5088
+ // The fresh-leaf check (`!== lastSendLeafId`) is what separates this degeneration
5089
+ // from a legitimate ack-then-stop where the model meant to wait for the user.
5090
+ function isEmptyStopAfterWillingnessAck(live: LiveSession): boolean {
5091
+ const leaf = live.session.sessionManager.getLeafEntry()
5092
+ if (!leaf || leaf.type !== 'message' || leaf.message.role !== 'assistant') return false
5093
+ if (leaf.message.stopReason !== 'stop') return false
5094
+ if (hasToolCall(leaf.message) || visibleAssistantText(leaf.message).trim() !== '') return false
5095
+ if (leaf.id === live.lastSendLeafId) return false
5096
+ const ackText = live.lastSentText.get(consecutiveSendKey(live.key.chat, live.key.thread))
5097
+ return ackText !== undefined && detectContinuationWillingness(ackText)
5098
+ }
5099
+
5024
5100
  function visibleAssistantText(message: AssistantMessage): string {
5025
5101
  return message.content
5026
5102
  .filter((block) => block.type === 'text')