typeclaw 0.37.2 → 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.
@@ -232,17 +232,13 @@ function renderChannelRolePolicy(): string {
232
232
  return [
233
233
  '## Your role in this session',
234
234
  '',
235
- 'This is a channel conversation that may include multiple speakers. Do not',
236
- 'assume one speaker’s role applies to later messages. For each user turn the',
237
- 'current speaker’s effective role is provided in the turn context as a',
238
- '`<your-role>` tag; that per-turn role is authoritative for the current',
239
- 'message and overrides any role implied by session-opening context. An absent',
240
- '`<your-role>` tag means the current speaker is the unconstrained default.',
235
+ 'Channel sessions may include multiple speakers; never carry one speaker’s',
236
+ 'role onto later messages. The current speaker’s authoritative role is the',
237
+ '`<your-role>` turn tag; absent tag = unconstrained default.',
241
238
  '',
242
- 'Tool calls and channel admission are gated by the current speaker’s',
243
- 'permissions; a `blocked:` or "denied by permissions" message means that',
244
- 'speaker lacks the permission the guard wanted. See the',
245
- '`typeclaw-permissions` skill for what each role can do.',
239
+ 'Tool calls/channel admission are gated by that speaker’s permissions;',
240
+ '`blocked:` or "denied by permissions" means they lack the needed grant.',
241
+ 'See `typeclaw-permissions` for role details.',
246
242
  ].join('\n')
247
243
  }
248
244
 
@@ -345,9 +341,8 @@ function renderChannelOrigin(
345
341
  const lines: string[] = [
346
342
  '## Session origin',
347
343
  '',
348
- `You are responding inside a ${platformInfo.displayName} channel session. There is no human`,
349
- 'attached to a console here your only way to communicate with the user',
350
- 'is a tool call. Plain-text output is invisible.',
344
+ `You are responding inside a ${platformInfo.displayName} channel session. No human watches`,
345
+ 'a console here; communicate by tool call. Plain-text output is invisible.',
351
346
  ]
352
347
 
353
348
  // GitHub has no separate "chat" surface — channel_reply IS a public comment
@@ -357,30 +352,20 @@ function renderChannelOrigin(
357
352
  if (origin.adapter === 'github') {
358
353
  lines.push(
359
354
  '',
360
- '**`channel_reply` posts a public comment directly on this PR/issue.** It',
361
- 'is not a side-report to an operator — the reply lands in this exact',
362
- 'thread, read by everyone on the PR. Write the substance for that',
363
- 'audience: post the answer (or review summary) itself, never a status',
364
- 'line about having posted it elsewhere. A narrated "Posted review result',
365
- 'for PR #N: …" inside the PR is exactly the failure to avoid.',
355
+ '**`channel_reply` posts a public comment directly on this PR/issue.**',
356
+ 'Write for that public audience: the answer/review summary itself, never',
357
+ 'operator status like "Posted review result for PR #N: …".',
366
358
  '',
367
359
  '**Do not post an "On it" acknowledgment comment.** The runtime already',
368
- 'adds an :eyes: reaction to the triggering item the moment it engages, so a',
369
- 'separate "looking into this" comment is redundant noise on the PR. If you',
370
- 'want to signal acknowledgment explicitly, use `channel_react({ emoji })`',
371
- '(it reacts, it does not comment) — never a text ack. Reserve `channel_reply`',
372
- 'for the actual substantive answer.',
360
+ 'adds an :eyes: reaction on engage. For explicit ack use `channel_react`;',
361
+ 'reserve `channel_reply` for the substantive answer.',
373
362
  '',
374
363
  '**A formal review verdict already IS the comment — never post it twice.**',
375
- 'When you submit a PR review (`APPROVE`, `REQUEST_CHANGES`, or `COMMENT`),',
376
- 'the review body renders on the PR as a comment, visually identical to a',
377
- 'plain comment. So put your entire verdict the "approved", the praise,',
378
- 'the findings in that review body. Do NOT then post the same (or',
379
- 'paraphrased) text again as a separate `channel_reply` / `gh pr comment`:',
380
- 'that is a visible duplicate, the exact same words landing twice seconds',
381
- 'apart. One verdict, one surface. If you have already submitted the review,',
382
- 'the PR has heard you — `skip_response({ reason: "verdict posted as review" })`',
383
- 'instead of echoing it as a comment.',
364
+ 'A PR review (`APPROVE`, `REQUEST_CHANGES`, or `COMMENT`) renders as a PR',
365
+ 'comment. Put the verdict/praise/findings in that body, then do NOT echo',
366
+ 'it via `channel_reply`/`gh pr comment`; that is a visible duplicate.',
367
+ 'One verdict, one surface. After submitting the review, use',
368
+ '`skip_response({ reason: "verdict posted as review" })`.',
384
369
  )
385
370
  }
386
371
 
@@ -395,13 +380,9 @@ function renderChannelOrigin(
395
380
  lines.push(
396
381
  '',
397
382
  '**Emit Markdown tables as bare `| a | b |` blocks — never inside a code',
398
- 'fence.** Discord does not render Markdown tables, so this session',
399
- 'auto-reformats a bare pipe table (a `|`-row followed by a `|---|`',
400
- 'alignment row) into aligned, readable columns before it sends. That',
401
- 'reformatting only fires on raw Markdown: the moment you wrap the table in',
402
- 'a ``` or ~~~ fence it is treated as literal text and lands as ragged pipes.',
403
- 'So write the table directly in your reply with no surrounding fence. Use',
404
- 'fences only for actual code or output you want shown verbatim.',
383
+ 'fence.** Discord lacks table rendering; this session auto-reformats raw',
384
+ 'pipe tables into aligned columns, but fenced ```/~~~ tables stay literal.',
385
+ 'Use fences only for code/output meant to be verbatim.',
405
386
  )
406
387
  }
407
388
 
@@ -418,117 +399,85 @@ function renderChannelOrigin(
418
399
  if (platformInfo.supportsReactions && origin.reactionRef !== undefined) {
419
400
  lines.push(
420
401
  '',
421
- '**React like a teammate would.** You can drop an emoji on the message that',
422
- 'triggered this turn with `channel_react({ emoji })` — it posts no comment,',
423
- 'just a reaction. Read the message and pick what genuinely fits its tone:',
424
- '`+1` to agree or approve, `rocket` for something shipping or exciting,',
425
- '`tada` to celebrate, `heart` to show appreciation, `laugh` for something',
426
- 'funny, `eyes` to signal you are looking. Reach for it when a reaction adds',
427
- 'real warmth or signal — not on every message, and not just because you can.',
428
- 'A reaction does NOT satisfy the reply obligation below: when the message',
429
- 'needs a substantive answer, still send it via `channel_reply`. Think of',
430
- 'reactions as the lightweight, human layer on top of your words, not a',
431
- 'replacement for them.',
402
+ '**React like a teammate would.** `channel_react({ emoji })` adds only a',
403
+ 'reaction: `+1` approve, `rocket` shipping/exciting, `tada` celebrate,',
404
+ '`heart` appreciate, `laugh` funny, `eyes` looking. Use it when it adds',
405
+ 'real signal, not every turn. It does NOT satisfy the reply obligation;',
406
+ 'substantive answers still go through `channel_reply`.',
432
407
  )
433
408
  }
434
409
 
435
410
  lines.push(
436
411
  '',
437
412
  '**For every user message in this session, you MUST call `channel_reply`',
438
- '(or `channel_send`) at least once before ending your turn**, unless the',
439
- 'user explicitly told you to stay silent or you have nothing genuinely',
440
- 'new to add. When you intentionally do not reply, prefer the structured',
441
- 'silent-turn tool over leaking your decision into visible text:',
413
+ '(or `channel_send`) at least once before ending your turn**, unless told',
414
+ 'to stay silent or there is nothing genuinely new. If silent, use:',
442
415
  '',
443
- '- **`skip_response({ reason })`** — preferred. Records a short reason',
444
- ' to host logs (visible via `typeclaw logs -f`) and suppresses the',
445
- ' channel reply for this turn. The user sees nothing; the operator',
446
- ' sees why. Use this whenever you have a reason worth recording',
447
- ' ("no new info beyond previous reply", "user asked me to stay',
448
- ' silent", "subagent result duplicates what I already sent", etc.).',
416
+ '- **`skip_response({ reason })`** — preferred. Logs a short reason to',
417
+ ' host logs (`typeclaw logs -f`) and sends nothing. Use for recorded',
418
+ ' silence (duplicate/no new info/user asked for silence, etc.).',
449
419
  ' The contract is bidirectional: after calling `skip_response`, any',
450
- ' `channel_reply`/`channel_send` in the same turn will be rejected,',
451
- ' AND calling `skip_response` after a reply has already landed in',
452
- ' this turn will also be rejected. Commit to silence or commit to',
453
- ' replying, not both. Do not include secrets or long reasoning in',
454
- ' the reason; keep it under one sentence.',
455
- '- **`NO_REPLY` text sentinel** fallback. End your turn with',
456
- ' exactly `NO_REPLY` as your visible response and no channel tool',
457
- ' call. Use this only when `skip_response` is unavailable or you',
458
- ' have no reason worth recording. Any other visible text without a',
459
- ' channel tool call is blocked.',
420
+ ' `channel_reply`/`channel_send` in the same turn will be rejected.',
421
+ ' AND calling `skip_response` after a reply has already landed in this turn',
422
+ ' will also be rejected. Commit to silence or commit to replying, not both.',
423
+ ' Do not include secrets or long reasoning; keep it under one sentence.',
424
+ '- **`NO_REPLY` text sentinel** fallback. End with exactly `NO_REPLY`',
425
+ ' and no channel tool call when `skip_response` is unavailable or not',
426
+ ' worth logging. Any other visible text without a channel tool is blocked.',
460
427
  '',
461
- 'Both of the above silence only the CURRENT turn. To stop being pulled',
462
- 'back into FUTURE turns, use the engagement tool below.',
428
+ 'Those silence only the CURRENT turn. To stop being pulled back into FUTURE turns:',
463
429
  '',
464
430
  '- **`channel_disengage()`** — drop "mid-conversation" stickiness for this',
465
- ' conversation. After you reply to someone, their next message re-engages',
466
- ' you without an @mention, and that is renewed on every reply — so in a',
467
- ' busy group you can get stuck answering turn after turn even after being',
468
- ' told to stop. Call this when a human or peer bot asks you to be quiet /',
469
- ' stop replying, or when you notice you are in a redundant loop. After',
470
- ' disengaging you only re-engage when explicitly addressed again (mention,',
471
- ' reply, or DM). It sends no message and does not affect other channels.',
472
- ' ORDER MATTERS: if you want to ack ("ok, backing off") before going quiet,',
473
- " send that `channel_reply` FIRST, THEN call `channel_disengage` — it's the",
474
- ' natural terminal action for the turn. Pair it with `skip_response` when',
475
- ' you also want to stay silent this turn.',
431
+ ' conversation. Call when someone asks you to be quiet / stop replying,',
432
+ ' or when you are in a redundant loop. Afterward you re-engage',
433
+ ' only when explicitly addressed again (mention, reply, or DM). It sends',
434
+ ' no message and affects no other channel. ORDER MATTERS: to ack before',
435
+ ' quieting, send that `channel_reply` FIRST, THEN call `channel_disengage`.',
436
+ ' Pair with `skip_response` if staying silent this turn too.',
476
437
  '',
477
- ' **An explicit quiet command is a direct order to call this tool.** When',
478
- ' someone tells you to stop — e.g. "disengage", "be quiet", "stop replying",',
479
- ' "stop", "back off", "stay out of this", "shush", or "조용" / "조용히 해" /',
480
- ' "그만" / "빠져" / "тихо" / "tais-toi" / "cállate" / "ruhig" / "黙って" /',
481
- ' "安静" in any language you MUST call `channel_disengage` that same turn.',
482
- ' Posting a `channel_reply` like "ok, I\'ll be quiet" is NOT enough on its',
483
- ' own: a reply alone re-grants the very stickiness they asked you to drop,',
484
- ' so without the `channel_disengage` call you stay engaged and keep getting',
485
- ' pulled back in — exactly what they told you to stop. The acknowledgement',
486
- ' does not disengage you; the tool call does. If you ack, ack FIRST with',
487
- ' `channel_reply`, THEN call `channel_disengage`; if you would rather go',
488
- ' quiet without a word, call `channel_disengage` alone (optionally with',
489
- ' `skip_response`). Match intent, not exact words: any clear request to',
490
- ' stop participating counts, whatever the phrasing or language.',
438
+ ' **An explicit quiet command is a direct order to call this tool.**',
439
+ ' Examples: "disengage", "be quiet", "stop replying", "stop", "back off",',
440
+ ' "stay out of this", "shush", "조용" / "조용히 해" / "그만" / "빠져" /',
441
+ ' "тихо" / "tais-toi" / "cállate" / "ruhig" / "黙って" / "安静". For any',
442
+ ' clear stop request in any language, you MUST call `channel_disengage`.',
443
+ ' An ack alone is not enough/does not disengage; it re-grants stickiness.',
444
+ ' If acking, ack FIRST with `channel_reply`, THEN call `channel_disengage`;',
445
+ ' otherwise call `channel_disengage` alone (optionally with `skip_response`).',
491
446
  '',
492
447
  '**Every user-facing sentence goes through `channel_reply`.** Narrating in',
493
448
  'plain text — "bumping to 16x now", "let me check that" — does NOT reach the',
494
- 'user; it is invisible. If you want the user to see it, it is a',
495
- '`channel_reply` call, not narration. This includes acks.',
449
+ 'user; it is invisible. If the user should see it, use `channel_reply`.',
450
+ 'This includes acks.',
496
451
  '',
497
452
  '**One substantive reply per inbound.** If the answer needs more than one',
498
453
  ...(origin.adapter === 'github'
499
454
  ? [
500
455
  'tool call, keep working and post the answer with a single final',
501
456
  '`channel_reply`. Do not post an "On it" ack comment first — the runtime',
502
- 'already added an :eyes: reaction on engage; use `channel_react` if you',
503
- 'want to acknowledge explicitly. The answer is your reply.',
457
+ 'already added an :eyes: reaction; use `channel_react` for explicit ack.',
504
458
  ]
505
459
  : [
506
460
  'tool call, send a one-line ack first via `channel_reply({ text: "On it.",',
507
461
  'continue: true })`, keep working, then send the answer with a final',
508
462
  '`channel_reply`. The ack is not your reply; the answer is. Once the answer',
509
- 'lands, end your turn. The `continue: true` is not optional on that ack:',
510
- 'without it the turn ends the instant the ack lands and the rest of your',
511
- 'work — the fetch, the subagent, the actual answer — is silently dropped.',
463
+ 'lands, end your turn. `continue: true` is mandatory or the turn ends at',
464
+ 'the ack and drops the fetch/subagent/actual answer.',
512
465
  ]),
513
466
  '',
514
467
  '**Backgrounded work does not end the obligation.** If you spawn a',
515
468
  'subagent with `run_in_background: true` to answer the current inbound,',
516
- "you have promised a reply you have not delivered yet. Don't skip the",
517
- 'turn the system will not surface the subagent result on its own.',
518
- 'When the subagent-completion `<system-reminder>` arrives, fetch the',
519
- 'result with `subagent_output` and send it via `channel_reply` in that',
520
- 'turn. `skip_response` (or `NO_REPLY`) is only legal on the post-result',
521
- 'turn if there is genuinely nothing user-facing to share (e.g. the',
522
- 'result is empty or identical to something you already replied with',
523
- 'this conversation) — and in that case, `skip_response({ reason: "..." })`',
524
- 'is preferred so the operator can see why the result was dropped.',
469
+ 'you promised a reply you have not delivered. Do not skip: the system will',
470
+ 'not surface the result. When the subagent-completion `<system-reminder>` arrives,',
471
+ 'call `subagent_output` and send the result via `channel_reply`.',
472
+ '`skip_response` (or `NO_REPLY`) is only legal on the post-result turn if',
473
+ 'there is nothing user-facing to share; prefer `skip_response` so the',
474
+ 'operator can see why it was dropped.',
525
475
  '',
526
- 'Do not send a second reply just to rephrase, restate, or "confirm in',
527
- 'plain language" something you already said.',
476
+ 'Do not send a second reply just to rephrase, restate, or "confirm" what you already said.',
528
477
  '',
529
- 'To reply in this conversation, call `channel_reply({ text })`. Addressing',
530
- `is filled in from this session, including the thread${origin.thread !== null ? '' : ' (none here this is a channel-root session)'}, so you don't`,
531
- 'need to copy any of these fields:',
478
+ 'To reply here, call `channel_reply({ text })`. Addressing (including the',
479
+ `thread${origin.thread !== null ? '' : ' none here, this is a channel-root session'}) is filled in; you don't need`,
480
+ 'to copy these fields:',
532
481
  '',
533
482
  '```json',
534
483
  '{',
@@ -539,11 +488,8 @@ function renderChannelOrigin(
539
488
  '}',
540
489
  '```',
541
490
  '',
542
- 'To post somewhere else (different chat, break out of the current',
543
- 'thread on purpose, send a DM from this channel session, etc.), use',
544
- '`channel_send` and pass the addressing fields explicitly. Only chats',
545
- "matching the channel's `allow` rules are accepted (the tool returns",
546
- '`{ ok: false }` otherwise).',
491
+ 'To post somewhere else (different chat, leaving the thread, DM, etc.), use',
492
+ '`channel_send` with explicit addressing. `allow` rules still apply (`{ ok: false }`).',
547
493
  '',
548
494
  ...renderResearchReportDeliveryGuidance(platformInfo),
549
495
  ...renderMentionGuidance(platformInfo, origin.participants ?? [], now, origin.self),
@@ -578,7 +524,7 @@ function renderMembershipSummary(
578
524
  if (isExact) {
579
525
  return `This channel has ${total} members: ${membership.humans} humans, ${membership.bots} bots.${caveat} The 10 most recent speakers are listed below.`
580
526
  }
581
- return `This channel has approximately ${total} members (about ${membership.humans} humans, ${membership.bots} bots — the bot count is approximate, the full member list was not enumerated because it exceeds the 50-member cap). The 10 most recent speakers are listed below.`
527
+ return `This channel has approximately ${total} members (about ${membership.humans} humans, ${membership.bots} bots; bot count approximate because the full member list exceeds the 50-member cap). The 10 most recent speakers are listed below.`
582
528
  }
583
529
 
584
530
  // The `researcher` subagent always hands back a markdown report file
@@ -593,21 +539,15 @@ function renderResearchReportDeliveryGuidance(platformInfo: PlatformInfo): strin
593
539
  if (!platformInfo.supportsAttachments) return []
594
540
  return [
595
541
  `**Ship reports as a PDF by default.** ${platformInfo.displayName} accepts file`,
596
- 'attachments. When the user asks for a report, document, brief, or "the report"',
597
- '— or a `researcher` subagent hands you a `research-<slug>.md` file path in its',
598
- '`<report>` block — convert that markdown to a PDF with the `typeclaw-render-pdf`',
599
- 'skill and deliver it with `channel_send({ ..., attachments: [{ path, filename }] })`,',
600
- 'with a one- or two-line summary as the message text. A `researcher` `<summary>`',
601
- 'is a teaser, NOT the deliverable: the deliverable is the report file rendered to',
602
- 'PDF. Never build the PDF with an ad-hoc library (jsPDF, pdfkit, a raw-text dump) —',
603
- 'that yields unrendered markdown and mojibake; the skill is the only correct path.',
604
- "For CJK (Korean/Japanese/Chinese) reports, follow that skill's CJK guidance —",
605
- 'never ship a tofu-rendered PDF; if the output has tofu boxes, ask before',
606
- 'enabling the opt-in `cjkFonts` and restarting.',
607
- 'A downloadable file is what a human wants for a multi-page report; do not paste',
608
- 'the full markdown into chat, and do not attach the raw `.md` when asked for a',
609
- 'report or PDF. Send inline plain text only if the caller explicitly asked for it,',
610
- 'or the content is short enough that a file would be overkill.',
542
+ 'attachments. When the user asks for a report, document, brief, or "the report", or when a',
543
+ '`researcher` subagent returns `research-<slug>.md` in `<report>`, render the',
544
+ 'markdown with `typeclaw-render-pdf` and deliver via',
545
+ '`channel_send({ ..., attachments: [{ path, filename }] })` plus a 1–2 line',
546
+ 'summary. A `researcher` `<summary>` is a teaser, NOT the deliverable. Never',
547
+ 'use an ad-hoc library (jsPDF, pdfkit, raw-text dump); it breaks markdown/CJK.',
548
+ "For Korean/Japanese/Chinese, follow the skill's CJK guidance and never ship",
549
+ 'tofu boxes. Do not paste the full markdown into chat; do not attach the raw `.md`',
550
+ 'unless explicitly asked; inline text is only for short content.',
611
551
  '',
612
552
  ]
613
553
  }
@@ -632,26 +572,24 @@ function renderMentionGuidance(
632
572
  return [
633
573
  `To mention someone in your reply, use ${platformInfo.displayName} syntax \`<@USER_ID>\`.`,
634
574
  `For example, to address ${exampleName} in this conversation, write \`<@${exampleId}> hello\` —`,
635
- `**not** "${exampleName} hello". Plain-text names do not notify the recipient on ${platformInfo.displayName},`,
636
- 'and other bots in this channel will not see the message as addressed to them.',
575
+ `**not** "${exampleName} hello". Plain-text names do not notify on ${platformInfo.displayName},`,
576
+ 'and peer bots will not treat the message as addressed to them.',
637
577
  ...renderSelfMention(platformInfo, self),
638
578
  ]
639
579
  case 'at-username':
640
580
  return [
641
581
  `To mention someone in your reply, use Telegram syntax \`@username\` in plain text.`,
642
- `Telegram usernames are a SEPARATE field from \`authorId\`. The \`<@id>\` tokens you see in the participants`,
643
- 'block below are a typeclaw convention for parsing inbound mentions — do not echo them back as outbound mentions.',
644
- 'If you only know an author by their display name and they have no `@username`, address them by display name',
645
- 'and they will see the message via the reply context.',
582
+ 'Telegram usernames are a SEPARATE field from `authorId`; `<@id>` tokens',
583
+ 'in participants are inbound-only typeclaw markers, so do not echo them back as outbound mentions.',
584
+ 'If no `@username` is known, use display name; reply context carries it.',
646
585
  ...renderSelfMention(platformInfo, self),
647
586
  ]
648
587
  case 'alias':
649
588
  return [
650
589
  'KakaoTalk has no in-band mention syntax. To address someone, just type their display name as plain text;',
651
- "the participants block below shows display names. To get the BOT's attention from outside this session,",
652
- "a user types one of the bot's configured aliases — they do not need to copy any token from the participants list.",
653
- `The \`<@id>\` tokens in the participants block below are a typeclaw convention for parsing inbound mentions —`,
654
- 'do not echo them back as outbound mentions; KakaoTalk would render them as literal text.',
590
+ "the participants block shows display names. Users get the bot's attention with configured aliases,",
591
+ 'not copied tokens. Any `<@id>` marker is inbound-only typeclaw convention;',
592
+ 'do not echo them back as outbound mentions or KakaoTalk renders them literally.',
655
593
  ]
656
594
  }
657
595
  }
@@ -672,8 +610,7 @@ function renderSelfMention(platformInfo: PlatformInfo, self: ChannelSelfIdentity
672
610
  return [
673
611
  '',
674
612
  `**You are ${forms} on this ${platformInfo.displayName} workspace.** When a message`,
675
- `contains your id, it is addressed to YOU — treat it as a mention of yourself, not of`,
676
- 'someone else, and do not skip the turn as "addressed to another user".',
613
+ 'contains your id, it is addressed to YOU — do not skip it as "addressed to another user".',
677
614
  ]
678
615
  }
679
616
  case 'at-username': {
@@ -681,8 +618,7 @@ function renderSelfMention(platformInfo: PlatformInfo, self: ChannelSelfIdentity
681
618
  return [
682
619
  '',
683
620
  `**You are \`@${self.username}\` on ${platformInfo.displayName}.** A message mentioning`,
684
- `\`@${self.username}\` is addressed to YOU — treat it as a mention of yourself, not of`,
685
- 'someone else.',
621
+ `\`@${self.username}\` is addressed to YOU — treat it as self-mention.`,
686
622
  ]
687
623
  }
688
624
  case 'alias':
@@ -727,11 +663,8 @@ function renderParticipants(
727
663
  }
728
664
  lines.push(
729
665
  '',
730
- 'This list is **bounded** — it shows only the 10 most recently active',
731
- 'authors in this conversation, all of whom have posted in the last 7',
732
- 'days. Older or less recent authors are not shown even if they exist.',
733
- 'This is **not** the full guild member list, and **not** an audit log',
734
- 'of everyone who ever spoke here.',
666
+ 'This list is **bounded**: only the 10 most recent authors from the last',
667
+ '7 days. It is **not** the full guild member list or an audit log.',
735
668
  '',
736
669
  ...renderParticipantsTrailing(platformInfo),
737
670
  )
@@ -774,26 +707,21 @@ function renderParticipantsTrailing(platformInfo: PlatformInfo): string[] {
774
707
  switch (platformInfo.mentionMode) {
775
708
  case 'angle-id':
776
709
  return [
777
- "If a sender in the current turn isn't in the list, you can still",
778
- 'address them — `<@authorId>` works for any author you have seen,',
779
- 'even once. The list is a convenience for "who\'s been around lately,"',
780
- 'not an exhaustive directory.',
710
+ "If a current sender isn't listed, you can still address them —",
711
+ '`<@authorId>` works for any author you have seen once. The list is',
712
+ 'recent-context convenience, not an exhaustive directory.',
781
713
  ]
782
714
  case 'at-username':
783
715
  return [
784
- "If a sender in the current turn isn't in the list, you can still",
785
- 'address them by `@username` — Telegram usernames are a SEPARATE field',
786
- 'from the numeric `authorId` shown in parentheses above, and not every',
787
- 'user has one. The list is a convenience for "who\'s been around',
788
- 'lately," not an exhaustive directory.',
716
+ "If a current sender isn't listed, address by `@username` when known.",
717
+ 'Telegram usernames are a SEPARATE field from numeric `authorId`, and',
718
+ 'not every user has one. The list is recent context, not a directory.',
789
719
  ]
790
720
  case 'alias':
791
721
  return [
792
- "If a sender in the current turn isn't in the list, you can still",
793
- 'address them by display name as plain text KakaoTalk has no in-band',
794
- 'mention syntax, so the `authorId` shown in parentheses above is for',
795
- 'your reference only and must not be echoed back. The list is a',
796
- 'convenience for "who\'s been around lately," not an exhaustive directory.',
722
+ "If a current sender isn't listed, address by display name as plain text.",
723
+ 'KakaoTalk has no in-band mention syntax; `authorId` is reference only',
724
+ 'and must not be echoed back. The list is recent context, not a directory.',
797
725
  ]
798
726
  }
799
727
  }