crewly 1.20.155 → 1.20.157

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.
Files changed (197) hide show
  1. package/config/skills/agent/core/attach-file/SKILL.md +1 -1
  2. package/config/skills/agent/core/attach-file/execute.sh +1 -1
  3. package/config/skills/agent/core/reply-channel/SKILL.md +4 -1
  4. package/config/skills/agent/core/reply-chat/SKILL.md +17 -0
  5. package/config/skills/agent/core/reply-chat/execute.sh +6 -1
  6. package/config/skills/agent/core/slack-post/SKILL.md +1 -1
  7. package/dist/backend/backend/src/constants.d.ts +195 -4
  8. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  9. package/dist/backend/backend/src/constants.js +208 -6
  10. package/dist/backend/backend/src/constants.js.map +1 -1
  11. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts +18 -0
  12. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  13. package/dist/backend/backend/src/controllers/chat/chat.controller.js +42 -6
  14. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  15. package/dist/backend/backend/src/controllers/owner-receipt/owner-receipt.controller.d.ts.map +1 -1
  16. package/dist/backend/backend/src/controllers/owner-receipt/owner-receipt.controller.js +5 -0
  17. package/dist/backend/backend/src/controllers/owner-receipt/owner-receipt.controller.js.map +1 -1
  18. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/slack/slack.controller.js +6 -1
  20. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/tickets/tickets.controller.d.ts +18 -0
  22. package/dist/backend/backend/src/controllers/tickets/tickets.controller.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/tickets/tickets.controller.js +42 -0
  24. package/dist/backend/backend/src/controllers/tickets/tickets.controller.js.map +1 -1
  25. package/dist/backend/backend/src/controllers/tickets/tickets.routes.d.ts +1 -0
  26. package/dist/backend/backend/src/controllers/tickets/tickets.routes.d.ts.map +1 -1
  27. package/dist/backend/backend/src/controllers/tickets/tickets.routes.js +3 -1
  28. package/dist/backend/backend/src/controllers/tickets/tickets.routes.js.map +1 -1
  29. package/dist/backend/backend/src/index.d.ts.map +1 -1
  30. package/dist/backend/backend/src/index.js +15 -2
  31. package/dist/backend/backend/src/index.js.map +1 -1
  32. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +1 -0
  33. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  34. package/dist/backend/backend/src/services/agent/agent-registration.service.js +18 -4
  35. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  36. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.d.ts +13 -0
  37. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.d.ts.map +1 -1
  38. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js +16 -0
  39. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
  40. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +30 -1
  41. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +46 -3
  43. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  44. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  45. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +17 -4
  46. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  47. package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts +93 -6
  48. package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
  49. package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js +169 -15
  50. package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
  51. package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts +2 -2
  52. package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/slack/slack-agent-post.service.js +15 -2
  54. package/dist/backend/backend/src/services/slack/slack-agent-post.service.js.map +1 -1
  55. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/slack/slack-initializer.js +7 -1
  57. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  58. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +40 -0
  59. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  60. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +86 -9
  61. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  62. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +1 -1
  63. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  64. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +33 -4
  65. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  66. package/dist/backend/backend/src/services/slack/slack-thread-context.service.d.ts +227 -0
  67. package/dist/backend/backend/src/services/slack/slack-thread-context.service.d.ts.map +1 -0
  68. package/dist/backend/backend/src/services/slack/slack-thread-context.service.js +455 -0
  69. package/dist/backend/backend/src/services/slack/slack-thread-context.service.js.map +1 -0
  70. package/dist/backend/backend/src/services/slack/slack-thread-key.d.ts +66 -0
  71. package/dist/backend/backend/src/services/slack/slack-thread-key.d.ts.map +1 -0
  72. package/dist/backend/backend/src/services/slack/slack-thread-key.js +109 -0
  73. package/dist/backend/backend/src/services/slack/slack-thread-key.js.map +1 -0
  74. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +89 -13
  75. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  76. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js +224 -50
  77. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  78. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt-data.d.ts +84 -0
  79. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt-data.d.ts.map +1 -1
  80. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt-data.js +301 -6
  81. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt-data.js.map +1 -1
  82. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.d.ts +8 -0
  83. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.d.ts.map +1 -1
  84. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.js +16 -0
  85. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.js.map +1 -1
  86. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.renderer.d.ts +28 -10
  87. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.renderer.d.ts.map +1 -1
  88. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.renderer.js +46 -193
  89. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.renderer.js.map +1 -1
  90. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.service.d.ts +15 -2
  91. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.service.d.ts.map +1 -1
  92. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.service.js +26 -4
  93. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.service.js.map +1 -1
  94. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.types.d.ts +35 -0
  95. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.types.d.ts.map +1 -1
  96. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.types.js.map +1 -1
  97. package/dist/backend/backend/src/services/v3/request.service.d.ts +2 -0
  98. package/dist/backend/backend/src/services/v3/request.service.d.ts.map +1 -1
  99. package/dist/backend/backend/src/services/v3/request.service.js +16 -4
  100. package/dist/backend/backend/src/services/v3/request.service.js.map +1 -1
  101. package/dist/backend/backend/src/services/v3/ticket-ask-classifier.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/v3/ticket-ask-classifier.js +2 -0
  103. package/dist/backend/backend/src/services/v3/ticket-ask-classifier.js.map +1 -1
  104. package/dist/backend/backend/src/services/v3/ticket-channel-hooks.d.ts.map +1 -1
  105. package/dist/backend/backend/src/services/v3/ticket-channel-hooks.js +2 -1
  106. package/dist/backend/backend/src/services/v3/ticket-channel-hooks.js.map +1 -1
  107. package/dist/backend/backend/src/services/v3/ticket-hygiene.d.ts +162 -0
  108. package/dist/backend/backend/src/services/v3/ticket-hygiene.d.ts.map +1 -0
  109. package/dist/backend/backend/src/services/v3/ticket-hygiene.js +221 -0
  110. package/dist/backend/backend/src/services/v3/ticket-hygiene.js.map +1 -0
  111. package/dist/backend/backend/src/services/v3/ticket-intake.service.d.ts +71 -6
  112. package/dist/backend/backend/src/services/v3/ticket-intake.service.d.ts.map +1 -1
  113. package/dist/backend/backend/src/services/v3/ticket-intake.service.js +151 -25
  114. package/dist/backend/backend/src/services/v3/ticket-intake.service.js.map +1 -1
  115. package/dist/backend/backend/src/services/v3/ticket-review.service.d.ts +22 -3
  116. package/dist/backend/backend/src/services/v3/ticket-review.service.d.ts.map +1 -1
  117. package/dist/backend/backend/src/services/v3/ticket-review.service.js +99 -40
  118. package/dist/backend/backend/src/services/v3/ticket-review.service.js.map +1 -1
  119. package/dist/backend/backend/src/types/slack.types.d.ts +43 -0
  120. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  121. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  122. package/dist/backend/backend/src/types/v2/request.types.d.ts +13 -0
  123. package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
  124. package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
  125. package/dist/backend/build-info.json +2 -2
  126. package/dist/cli/backend/src/constants.d.ts +195 -4
  127. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  128. package/dist/cli/backend/src/constants.js +208 -6
  129. package/dist/cli/backend/src/constants.js.map +1 -1
  130. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +1 -0
  131. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  132. package/dist/cli/backend/src/services/agent/agent-registration.service.js +18 -4
  133. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  134. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.d.ts +13 -0
  135. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.d.ts.map +1 -1
  136. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js +16 -0
  137. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
  138. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +30 -1
  139. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  140. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +46 -3
  141. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  142. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  143. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +17 -4
  144. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  145. package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts +93 -6
  146. package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
  147. package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js +169 -15
  148. package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
  149. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +40 -0
  150. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  151. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +86 -9
  152. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  153. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +1 -1
  154. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  155. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +33 -4
  156. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  157. package/dist/cli/backend/src/services/slack/slack-thread-context.service.d.ts +227 -0
  158. package/dist/cli/backend/src/services/slack/slack-thread-context.service.d.ts.map +1 -0
  159. package/dist/cli/backend/src/services/slack/slack-thread-context.service.js +455 -0
  160. package/dist/cli/backend/src/services/slack/slack-thread-context.service.js.map +1 -0
  161. package/dist/cli/backend/src/services/slack/slack-thread-key.d.ts +66 -0
  162. package/dist/cli/backend/src/services/slack/slack-thread-key.d.ts.map +1 -0
  163. package/dist/cli/backend/src/services/slack/slack-thread-key.js +109 -0
  164. package/dist/cli/backend/src/services/slack/slack-thread-key.js.map +1 -0
  165. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +89 -13
  166. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  167. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js +224 -50
  168. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  169. package/dist/cli/backend/src/services/v3/request.service.d.ts +2 -0
  170. package/dist/cli/backend/src/services/v3/request.service.d.ts.map +1 -1
  171. package/dist/cli/backend/src/services/v3/request.service.js +16 -4
  172. package/dist/cli/backend/src/services/v3/request.service.js.map +1 -1
  173. package/dist/cli/backend/src/services/v3/ticket-ask-classifier.d.ts.map +1 -1
  174. package/dist/cli/backend/src/services/v3/ticket-ask-classifier.js +2 -0
  175. package/dist/cli/backend/src/services/v3/ticket-ask-classifier.js.map +1 -1
  176. package/dist/cli/backend/src/services/v3/ticket-channel-hooks.d.ts.map +1 -1
  177. package/dist/cli/backend/src/services/v3/ticket-channel-hooks.js +2 -1
  178. package/dist/cli/backend/src/services/v3/ticket-channel-hooks.js.map +1 -1
  179. package/dist/cli/backend/src/services/v3/ticket-hygiene.d.ts +162 -0
  180. package/dist/cli/backend/src/services/v3/ticket-hygiene.d.ts.map +1 -0
  181. package/dist/cli/backend/src/services/v3/ticket-hygiene.js +221 -0
  182. package/dist/cli/backend/src/services/v3/ticket-hygiene.js.map +1 -0
  183. package/dist/cli/backend/src/services/v3/ticket-intake.service.d.ts +71 -6
  184. package/dist/cli/backend/src/services/v3/ticket-intake.service.d.ts.map +1 -1
  185. package/dist/cli/backend/src/services/v3/ticket-intake.service.js +151 -25
  186. package/dist/cli/backend/src/services/v3/ticket-intake.service.js.map +1 -1
  187. package/dist/cli/backend/src/services/v3/ticket-review.service.d.ts +22 -3
  188. package/dist/cli/backend/src/services/v3/ticket-review.service.d.ts.map +1 -1
  189. package/dist/cli/backend/src/services/v3/ticket-review.service.js +99 -40
  190. package/dist/cli/backend/src/services/v3/ticket-review.service.js.map +1 -1
  191. package/dist/cli/backend/src/types/slack.types.d.ts +43 -0
  192. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  193. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  194. package/dist/cli/backend/src/types/v2/request.types.d.ts +13 -0
  195. package/dist/cli/backend/src/types/v2/request.types.d.ts.map +1 -1
  196. package/dist/cli/backend/src/types/v2/request.types.js.map +1 -1
  197. package/package.json +1 -1
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Ticket hygiene (specs/ticket-calm.md) — the rules that keep the ticket pile
3
+ * from growing on the owner:
4
+ *
5
+ * - {@link answerNeedsOwner}: an answered ticket waits in 待验收 only when it
6
+ * has a deliverable someone must look at (a document, an email, a draft, a
7
+ * form, code, a deploy, money, anything sent out) or when the answer asks
8
+ * the owner something. A plain answer closes on the spot.
9
+ * - {@link isStaleTicket}: open / running with no activity for
10
+ * {@link TICKET_CONSTANTS.STALE.AFTER_MS} → closed as stale.
11
+ * - {@link planTicketCleanup} / {@link runTicketCleanup}: the one-time
12
+ * cleanup of the pile that built up before these rules (dry run by
13
+ * default, idempotent).
14
+ *
15
+ * Pure except for {@link runTicketCleanup}, which writes through the
16
+ * request store it is given. No LLM.
17
+ *
18
+ * @module services/v3/ticket-hygiene
19
+ */
20
+ import { TICKET_CONSTANTS } from '../../constants.js';
21
+ import { formatTicketNumber, ticketNeedsReview } from '../../types/v2/ticket.types.js';
22
+ import { askText } from './ticket-ask-classifier.js';
23
+ // ---------------------------------------------------------------------------
24
+ // Rules
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Whether an answered ticket has to wait for the owner (待验收) or can close
28
+ * as soon as it is answered.
29
+ *
30
+ * Waits when the ask names a deliverable ({@link TICKET_CONSTANTS.REVIEW.DELIVERABLE_ASK}),
31
+ * the intent is a change ({@link TICKET_CONSTANTS.REVIEW.DELIVERABLE_CATEGORIES}),
32
+ * or the agent's answer asks the owner something
33
+ * ({@link TICKET_CONSTANTS.REVIEW.OWNER_QUESTION}). Everything else — "看看这个",
34
+ * "研究一下", "这个团队都有几个人" — is a conversation, and a conversation
35
+ * that got its answer is done.
36
+ *
37
+ * @param ticket - The ticket (its ask, intent and latest answer)
38
+ * @returns True when the owner has to look at the result
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * answerNeedsOwner({ description: '帮我写一封回信', reply: {...} }); // true
43
+ * answerNeedsOwner({ description: '看看这个 https://x.com/…', reply: {...} }); // false
44
+ * ```
45
+ */
46
+ export function answerNeedsOwner(ticket) {
47
+ if (ticket.kind === 'question')
48
+ return false;
49
+ const R = TICKET_CONSTANTS.REVIEW;
50
+ if (ticket.intentCategory && R.DELIVERABLE_CATEGORIES.includes(ticket.intentCategory))
51
+ return true;
52
+ if (R.DELIVERABLE_ASK.test(askText(ticket.description ?? '')))
53
+ return true;
54
+ const answer = (ticket.reply?.excerpt ?? '').trim();
55
+ // Ends on a question to him, or asks for an OK / a choice anywhere.
56
+ return /[??]$/.test(answer) || R.OWNER_QUESTION.test(answer);
57
+ }
58
+ /**
59
+ * The last time anything happened on a ticket: created, updated, answered,
60
+ * submitted or discussed.
61
+ *
62
+ * @param ticket - The ticket
63
+ * @returns Epoch ms (NaN-safe: unparsable dates are skipped)
64
+ */
65
+ export function ticketLastActivity(ticket) {
66
+ const times = [ticket.createdAt, ticket.updatedAt, ticket.submittedAt, ticket.reply?.at, ...(ticket.discussion ?? []).map((d) => d.at)]
67
+ .map((t) => (t ? Date.parse(t) : NaN))
68
+ .filter((n) => Number.isFinite(n));
69
+ return times.length > 0 ? Math.max(...times) : 0;
70
+ }
71
+ /**
72
+ * Whether a ticket has gone stale: open / ready / running and untouched for
73
+ * {@link TICKET_CONSTANTS.STALE.AFTER_MS}.
74
+ *
75
+ * @param ticket - The ticket
76
+ * @param now - Current time (ms)
77
+ * @returns True when it should be closed as stale
78
+ */
79
+ export function isStaleTicket(ticket, now) {
80
+ if (!TICKET_CONSTANTS.STALE.STATUSES.includes(ticket.status))
81
+ return false;
82
+ return now - ticketLastActivity(ticket) >= TICKET_CONSTANTS.STALE.AFTER_MS;
83
+ }
84
+ /**
85
+ * The update that closes a ticket as stale: cancelled, tagged, with a note in
86
+ * its discussion so whoever opens it sees why.
87
+ *
88
+ * @param ticket - The ticket
89
+ * @param nowIso - Current time
90
+ * @returns The update
91
+ */
92
+ export function staleCloseUpdate(ticket, nowIso) {
93
+ const note = {
94
+ at: nowIso,
95
+ author: TICKET_CONSTANTS.STALE.NOTE_AUTHOR,
96
+ text: TICKET_CONSTANTS.STALE.NOTE,
97
+ ref: `stale-${nowIso}`,
98
+ };
99
+ return {
100
+ status: 'cancelled',
101
+ tags: [...new Set([...ticket.tags, TICKET_CONSTANTS.STALE.TAG])],
102
+ discussion: [...(ticket.discussion ?? []), note],
103
+ };
104
+ }
105
+ /**
106
+ * Plan the one-time cleanup — the 2026-09-28 rules applied to the pile that
107
+ * built up before them:
108
+ *
109
+ * - a 待验收 ticket whose answer needs nothing from the owner
110
+ * ({@link answerNeedsOwner}) is closed as answered;
111
+ * - any other 待验收 ticket answered more than
112
+ * {@link TICKET_CONSTANTS.REVIEW.AUTO_ACCEPT_MS} ago is accepted (silence);
113
+ * - an open / running one idle for {@link TICKET_CONSTANTS.STALE.AFTER_MS} is
114
+ * closed as stale.
115
+ *
116
+ * Idempotent: a second plan over the result is empty.
117
+ *
118
+ * @param requests - Every Request in the store
119
+ * @param opts - Clock and scope
120
+ * @returns The report (not applied)
121
+ */
122
+ export function planTicketCleanup(requests, opts) {
123
+ const actions = [];
124
+ for (const r of requests) {
125
+ const numbered = typeof r.ticketNumber === 'number';
126
+ if (!numbered && opts.includeLegacy === false)
127
+ continue;
128
+ const base = { id: r.id, tkt: numbered ? formatTicketNumber(r.ticketNumber) : null, title: r.title, from: r.status };
129
+ if (r.status === 'waiting_confirmation') {
130
+ const since = Date.parse(r.submittedAt ?? r.updatedAt);
131
+ if (numbered && ticketNeedsReview(r) && r.reply && !answerNeedsOwner(r))
132
+ actions.push({ ...base, action: 'answered' });
133
+ else if (Number.isFinite(since) && opts.now - since >= TICKET_CONSTANTS.REVIEW.AUTO_ACCEPT_MS)
134
+ actions.push({ ...base, action: 'accept' });
135
+ }
136
+ else if (isStaleTicket(r, opts.now)) {
137
+ actions.push({ ...base, action: 'stale' });
138
+ }
139
+ }
140
+ return {
141
+ applied: false,
142
+ scanned: requests.length,
143
+ answered: actions.filter((a) => a.action === 'answered').length,
144
+ accept: actions.filter((a) => a.action === 'accept').length,
145
+ stale: actions.filter((a) => a.action === 'stale').length,
146
+ failed: [],
147
+ actions,
148
+ };
149
+ }
150
+ /**
151
+ * Run the cleanup. Dry run unless `apply`; the plan is made from the store's
152
+ * current contents, so running it twice changes nothing the second time.
153
+ *
154
+ * Accepting goes through the store's `done` gate with `accepted` (silence,
155
+ * tagged `auto_accepted`) and `ignoreDeadChildren`, so a ticket with a live
156
+ * WorkItem still stays open (reported under `failed`). A `waiting_confirmation`
157
+ * ticket that does not need review (a question, a cron ticket) is closed the
158
+ * same way without the tag.
159
+ *
160
+ * @param store - Request store
161
+ * @param opts - Clock, scope and `apply`
162
+ * @returns What was (or would be) done
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * const dry = await runTicketCleanup(RequestService.getInstance(), { now: Date.now() });
167
+ * const done = await runTicketCleanup(RequestService.getInstance(), { now: Date.now(), apply: true });
168
+ * ```
169
+ */
170
+ export async function runTicketCleanup(store, opts) {
171
+ const all = await store.listAll();
172
+ const plan = planTicketCleanup(all, opts);
173
+ if (!opts.apply)
174
+ return plan;
175
+ const byId = new Map(all.map((r) => [r.id, r]));
176
+ const nowIso = new Date(opts.now).toISOString();
177
+ const failed = [];
178
+ for (const a of plan.actions) {
179
+ const r = byId.get(a.id);
180
+ if (!r)
181
+ continue;
182
+ try {
183
+ if (a.action === 'answered') {
184
+ await store.update(r.id, {
185
+ status: 'done',
186
+ accepted: true,
187
+ ignoreDeadChildren: true,
188
+ tags: [...new Set([...r.tags, TICKET_CONSTANTS.REVIEW.ANSWERED_TAG])],
189
+ });
190
+ }
191
+ else if (a.action === 'accept') {
192
+ const silence = ticketNeedsReview(r);
193
+ await store.update(r.id, {
194
+ status: 'done',
195
+ accepted: true,
196
+ ignoreDeadChildren: true,
197
+ ...(silence
198
+ ? { acceptedBy: 'silence', tags: [...new Set([...r.tags, TICKET_CONSTANTS.REVIEW.AUTO_ACCEPTED_TAG])] }
199
+ : {}),
200
+ });
201
+ }
202
+ else {
203
+ await store.update(r.id, staleCloseUpdate(r, nowIso));
204
+ }
205
+ }
206
+ catch (err) {
207
+ failed.push({ id: r.id, reason: err instanceof Error ? err.message : String(err) });
208
+ }
209
+ }
210
+ const failedIds = new Set(failed.map((f) => f.id));
211
+ const ok = plan.actions.filter((a) => !failedIds.has(a.id));
212
+ return {
213
+ ...plan,
214
+ applied: true,
215
+ answered: ok.filter((a) => a.action === 'answered').length,
216
+ accept: ok.filter((a) => a.action === 'accept').length,
217
+ stale: ok.filter((a) => a.action === 'stale').length,
218
+ failed,
219
+ };
220
+ }
221
+ //# sourceMappingURL=ticket-hygiene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticket-hygiene.js","sourceRoot":"","sources":["../../../../../../backend/src/services/v3/ticket-hygiene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAA8B,MAAM,gCAAgC,CAAC;AACnH,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAErD,8EAA8E;AAC9E,QAAQ;AACR,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAyI;IAEzI,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IAC7C,MAAM,CAAC,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAClC,IAAI,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC,sBAAsB,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC;QAAE,OAAO,IAAI,CAAC;IACnG,IAAI,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,oEAAoE;IACpE,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAmJ;IAEnJ,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SACpI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;SACrC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAe,EAAE,GAAW;IACxD,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3E,OAAO,GAAG,GAAG,kBAAkB,CAAC,MAAM,CAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC;AAC7E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAe,EAAE,MAAc;IAC9D,MAAM,IAAI,GAA0B;QAClC,EAAE,EAAE,MAAM;QACV,MAAM,EAAE,gBAAgB,CAAC,KAAK,CAAC,WAAW;QAC1C,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,IAAI;QACjC,GAAG,EAAE,SAAS,MAAM,EAAE;KACvB,CAAC;IACF,OAAO;QACL,MAAM,EAAE,WAAW;QACnB,IAAI,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAChE,UAAU,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC;KACjD,CAAC;AACJ,CAAC;AAkDD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAA4B,EAAE,IAA0B;IACxF,MAAM,OAAO,GAA0B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,YAAY,KAAK,QAAQ,CAAC;QACpD,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,aAAa,KAAK,KAAK;YAAE,SAAS;QACxD,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,YAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;QAC/H,IAAI,CAAC,CAAC,MAAM,KAAK,sBAAsB,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC;YACvD,IAAI,QAAQ,IAAI,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;iBAClH,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,GAAG,KAAK,IAAI,gBAAgB,CAAC,MAAM,CAAC,cAAc;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7I,CAAC;aAAM,IAAI,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACtC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,OAAO;QACL,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,QAAQ,CAAC,MAAM;QACxB,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,MAAM;QAC/D,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM;QAC3D,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,MAAM;QACzD,MAAM,EAAE,EAAE;QACV,OAAO;KACR,CAAC;AACJ,CAAC;AAQD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAyB,EACzB,IAAgD;IAEhD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAChD,MAAM,MAAM,GAAkC,EAAE,CAAC;IACjD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,IAAI,CAAC;YACH,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBAC5B,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;oBACvB,MAAM,EAAE,MAAM;oBACd,QAAQ,EAAE,IAAI;oBACd,kBAAkB,EAAE,IAAI;oBACxB,IAAI,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;iBACtE,CAAC,CAAC;YACL,CAAC;iBAAM,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACjC,MAAM,OAAO,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;gBACrC,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;oBACvB,MAAM,EAAE,MAAM;oBACd,QAAQ,EAAE,IAAI;oBACd,kBAAkB,EAAE,IAAI;oBACxB,GAAG,CAAC,OAAO;wBACT,CAAC,CAAC,EAAE,UAAU,EAAE,SAAkB,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE;wBAChH,CAAC,CAAC,EAAE,CAAC;iBACR,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,gBAAgB,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACnD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,OAAO;QACL,GAAG,IAAI;QACP,OAAO,EAAE,IAAI;QACb,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,MAAM;QAC1D,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM;QACtD,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,MAAM;QACpD,MAAM;KACP,CAAC;AACJ,CAAC"}
@@ -10,9 +10,18 @@
10
10
  *
11
11
  * - opens a new ticket (a `Request` with a TKT number, `kind`, `origin`,
12
12
  * `assignee`) and posts ONE receipt where it was said;
13
- * - is a reply in a thread that already has a ticket: a follow-up on that
14
- * work is appended to its discussion; a NEW ask opens its own ticket in the
15
- * same thread, linked to the parent (#827, {@link classifyOwnerMessage});
13
+ * - is a reply in a thread that already has a ticket — open, waiting for the
14
+ * owner, or finished within {@link TICKET_CONSTANTS.FOLLOW_UP.RECENT_TICKET_MS}:
15
+ * a follow-up, an answer or a decision is appended to its discussion (and
16
+ * on a 待验收 ticket counts as the owner's OK); only a genuinely new ask
17
+ * opens its own ticket in the same thread, linked to the parent (#827,
18
+ * {@link isGenuinelyNewAsk});
19
+ * - is a DM reply soon after the conversation's latest ticket
20
+ * ({@link TICKET_CONSTANTS.FOLLOW_UP.DM_WINDOW_MS}): the same, for
21
+ * conversations where every message is top level;
22
+ * - is a reply in a thread an agent started (no ticket): an answer to the
23
+ * agent, ignored unless it is a genuinely new ask (2026-09-28: 「发了」
24
+ * 「A 论文那个 开个Issue吧」 had become tickets of their own);
16
25
  * - is "不用记" (don't track) and dismisses the ticket it answers;
17
26
  * - is noise (trivial ack, file only, a status ping, agent-authored) and is
18
27
  * ignored. Request-phrased questions ("可以去研究一下 X 吗") are asks, and a
@@ -23,7 +32,7 @@
23
32
  *
24
33
  * @module services/v3/ticket-intake.service
25
34
  */
26
- import { weightedTextLength } from './ticket-ask-classifier.js';
35
+ import { weightedTextLength, type AskClassification } from './ticket-ask-classifier.js';
27
36
  import type { IntakeOutcomeRecorder } from './ticket-intake-log.js';
28
37
  import { type Request, type RequestStatus, type TicketAcceptedBy } from '../../types/v2/request.types.js';
29
38
  import { type TicketOrigin, type TicketReceipt, type TicketKind, type TicketBoardColumn, type TicketPriorityLabel, type BoardWorkItemView, type TicketAcceptance, type TicketReply } from '../../types/v2/ticket.types.js';
@@ -60,6 +69,26 @@ export declare function titleText(text: string): string;
60
69
  * @returns True for "不用记" and its variants
61
70
  */
62
71
  export declare function isDismissText(text: string): boolean;
72
+ /**
73
+ * Whether a reply in an existing conversation (a ticket's thread, an agent's
74
+ * thread, a DM right after a ticket) is a genuinely new ask that deserves its
75
+ * own ticket (2026-09-28). Stricter than the classifier's `new_ask` alone:
76
+ *
77
+ * - a question stays in the conversation (the agent answers it there);
78
+ * - a short reply (≤ {@link TICKET_CONSTANTS.FOLLOW_UP.SHORT_REPLY_WEIGHTED_LENGTH}
79
+ * weighted) is an answer — 「可以改到10:30吗」 — unless it says 「帮我…」
80
+ * outright or opens a new topic.
81
+ *
82
+ * @param text - The owner's message
83
+ * @param ask - Its in-thread classification
84
+ * @returns True when it opens a ticket of its own
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * isGenuinelyNewAsk('A 论文那个 开个Issue吧 放到backlog', classifyOwnerMessage(t, { inThread: true })); // false
89
+ * ```
90
+ */
91
+ export declare function isGenuinelyNewAsk(text: string, ask: AskClassification): boolean;
63
92
  /** Where a receipt should be posted. */
64
93
  export type ReceiptTarget = {
65
94
  kind: 'slack';
@@ -109,6 +138,11 @@ export interface IntakeMessage {
109
138
  legacyThreadParentRef?: string;
110
139
  /** Extra tags (e.g. `slack` / `chat-v2` for the SLA subscriber) */
111
140
  tags?: readonly string[];
141
+ /**
142
+ * The message is a reply inside a thread (not the thread's root). A reply
143
+ * in a thread with no ticket answers whoever started it — an agent.
144
+ */
145
+ isThreadReply?: boolean;
112
146
  /** Where to post the receipt; omitted = no receipt */
113
147
  receipt?: ReceiptTarget;
114
148
  }
@@ -445,11 +479,42 @@ export declare class TicketIntakeService {
445
479
  * @returns True when it was
446
480
  */
447
481
  private conversationMatches;
482
+ /**
483
+ * Whether a finished ticket finished recently enough that its thread is
484
+ * still its conversation ({@link TICKET_CONSTANTS.FOLLOW_UP.RECENT_TICKET_MS}).
485
+ *
486
+ * @param ticket - A done or cancelled ticket
487
+ * @returns True when follow-ups in its thread still belong to it
488
+ */
489
+ private finishedRecently;
490
+ /**
491
+ * In a DM-like conversation (every message top level), the ticket a reply
492
+ * most likely follows up: the conversation's ticket with the latest
493
+ * activity, if that was within {@link TICKET_CONSTANTS.FOLLOW_UP.DM_WINDOW_MS}.
494
+ * Dismissed tickets never take follow-ups.
495
+ *
496
+ * @param all - Every Request
497
+ * @param message - The message
498
+ * @returns The ticket, or null
499
+ */
500
+ private recentConversationTicket;
501
+ /**
502
+ * An owner follow-up on a 待验收 ticket that is not 打回 (2026-09-28): it is
503
+ * his OK. The ticket is accepted and the message kept in its discussion;
504
+ * when it cannot be accepted yet (live work), the agent is back on it.
505
+ *
506
+ * @param ticket - The 待验收 ticket
507
+ * @param message - The follow-up
508
+ * @param text - Trimmed text
509
+ * @returns `verified`, or `appended` when it could not be accepted
510
+ */
511
+ private acceptOnFollowUp;
448
512
  /**
449
513
  * Append a follow-up to a ticket's discussion.
450
514
  *
451
- * Trivial follow-ups ("好的") are not written, but the message still belongs
452
- * to the ticket (so the delivered copy carries its marker).
515
+ * Pure acknowledgements ("好的", "thanks") are not written, but the message
516
+ * still belongs to the ticket (so the delivered copy carries its marker).
517
+ * Short answers are written: 「是绿卡」 is what the agent asked for.
453
518
  *
454
519
  * @param ticket - The open ticket
455
520
  * @param message - The follow-up
@@ -1 +1 @@
1
- {"version":3,"file":"ticket-intake.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/v3/ticket-intake.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ticket-intake.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/v3/ticket-intake.service.ts"],"names":[],"mappings":"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"}
@@ -10,9 +10,18 @@
10
10
  *
11
11
  * - opens a new ticket (a `Request` with a TKT number, `kind`, `origin`,
12
12
  * `assignee`) and posts ONE receipt where it was said;
13
- * - is a reply in a thread that already has a ticket: a follow-up on that
14
- * work is appended to its discussion; a NEW ask opens its own ticket in the
15
- * same thread, linked to the parent (#827, {@link classifyOwnerMessage});
13
+ * - is a reply in a thread that already has a ticket — open, waiting for the
14
+ * owner, or finished within {@link TICKET_CONSTANTS.FOLLOW_UP.RECENT_TICKET_MS}:
15
+ * a follow-up, an answer or a decision is appended to its discussion (and
16
+ * on a 待验收 ticket counts as the owner's OK); only a genuinely new ask
17
+ * opens its own ticket in the same thread, linked to the parent (#827,
18
+ * {@link isGenuinelyNewAsk});
19
+ * - is a DM reply soon after the conversation's latest ticket
20
+ * ({@link TICKET_CONSTANTS.FOLLOW_UP.DM_WINDOW_MS}): the same, for
21
+ * conversations where every message is top level;
22
+ * - is a reply in a thread an agent started (no ticket): an answer to the
23
+ * agent, ignored unless it is a genuinely new ask (2026-09-28: 「发了」
24
+ * 「A 论文那个 开个Issue吧」 had become tickets of their own);
16
25
  * - is "不用记" (don't track) and dismisses the ticket it answers;
17
26
  * - is noise (trivial ack, file only, a status ping, agent-authored) and is
18
27
  * ignored. Request-phrased questions ("可以去研究一下 X 吗") are asks, and a
@@ -27,7 +36,8 @@ import * as path from 'path';
27
36
  import { LoggerService } from '../core/logger.service.js';
28
37
  import { modifyJsonFile } from '../../utils/file-io.utils.js';
29
38
  import { TICKET_CONSTANTS } from '../../constants.js';
30
- import { classifyOwnerMessage, weightedTextLength } from './ticket-ask-classifier.js';
39
+ import { askText, classifyOwnerMessage, weightedTextLength } from './ticket-ask-classifier.js';
40
+ import { ticketLastActivity } from './ticket-hygiene.js';
31
41
  import { redactSensitive } from '../wiki/wiki-redaction.js';
32
42
  import { TERMINAL_REQUEST_STATUSES, isValidRequestTransition, } from '../../types/v2/request.types.js';
33
43
  import { formatTicketNumber, parseTicketNumber, deriveBoardColumn, ticketPriorityLabel, inferTicketKind, uniqueTicketIdFromTexts, parseReviewReply, activeAcceptance, ticketNeedsReview, } from '../../types/v2/ticket.types.js';
@@ -37,10 +47,13 @@ import { formatTicketNumber, parseTicketNumber, deriveBoardColumn, ticketPriorit
37
47
  // ---------------------------------------------------------------------------
38
48
  /**
39
49
  * Trimmed-text regex for trivial acknowledgement messages — anchored,
40
- * case-insensitive, allows trailing punctuation. Matches "ok", "好的",
41
- * "thx", "👍", etc.
50
+ * case-insensitive, allows trailing punctuation, spaces and emoji. Matches
51
+ * "ok", "好的", "thx", "👍", etc.
52
+ *
53
+ * The tail used to be `\W*`, and CJK characters are all `\W`: 「是绿卡」
54
+ * 「对 就按第二种来 不用再问了」 read as a bare 「是」/「对」 and were dropped.
42
55
  */
43
- const TRIVIAL_ACK_PATTERN = /^(ok|okay|好的|好|收到|thx|thanks|thank you|谢谢|多谢|👍|✅|got it|sure|yes|是|对|对的|嗯|嗯嗯|行)\W*$/iu;
56
+ const TRIVIAL_ACK_PATTERN = /^(ok|okay|好的|好|收到|thx|thanks|thank you|谢谢|多谢|👍|✅|got it|sure|yes|是|对|对的|嗯|嗯嗯|行)[\s\p{P}\p{S}]*$/iu;
44
57
  /**
45
58
  * Trimmed-text regex matching the synthetic `[Slack File: …]` lines the
46
59
  * Slack bridge appends for uploaded files.
@@ -105,6 +118,32 @@ export function titleText(text) {
105
118
  export function isDismissText(text) {
106
119
  return TICKET_CONSTANTS.DISMISS_PATTERN.test(text);
107
120
  }
121
+ /**
122
+ * Whether a reply in an existing conversation (a ticket's thread, an agent's
123
+ * thread, a DM right after a ticket) is a genuinely new ask that deserves its
124
+ * own ticket (2026-09-28). Stricter than the classifier's `new_ask` alone:
125
+ *
126
+ * - a question stays in the conversation (the agent answers it there);
127
+ * - a short reply (≤ {@link TICKET_CONSTANTS.FOLLOW_UP.SHORT_REPLY_WEIGHTED_LENGTH}
128
+ * weighted) is an answer — 「可以改到10:30吗」 — unless it says 「帮我…」
129
+ * outright or opens a new topic.
130
+ *
131
+ * @param text - The owner's message
132
+ * @param ask - Its in-thread classification
133
+ * @returns True when it opens a ticket of its own
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * isGenuinelyNewAsk('A 论文那个 开个Issue吧 放到backlog', classifyOwnerMessage(t, { inThread: true })); // false
138
+ * ```
139
+ */
140
+ export function isGenuinelyNewAsk(text, ask) {
141
+ if (ask.verdict !== 'new_ask')
142
+ return false;
143
+ if (ask.signals.includes('strong_request') || ask.signals.includes('new_topic'))
144
+ return true;
145
+ return weightedTextLength(askText(text)) > TICKET_CONSTANTS.FOLLOW_UP.SHORT_REPLY_WEIGHTED_LENGTH;
146
+ }
108
147
  // ---------------------------------------------------------------------------
109
148
  // Service
110
149
  // ---------------------------------------------------------------------------
@@ -259,8 +298,9 @@ export class TicketIntakeService {
259
298
  return this.ignored(message, `dismiss_${result.reason}`);
260
299
  return { action: 'dismissed', ticket: result.ticket };
261
300
  }
262
- // Same message twice (Slack redelivery, two event types).
263
- const duplicate = all.find((r) => r.sourceConversationItemId === message.origin.ref);
301
+ // Same message twice (Slack redelivery, two event types) — whether it
302
+ // opened a ticket or was appended to one.
303
+ const duplicate = all.find((r) => r.sourceConversationItemId === message.origin.ref || (r.discussion ?? []).some((d) => d.ref === message.origin.ref));
264
304
  if (duplicate)
265
305
  return { action: 'duplicate', ticket: duplicate };
266
306
  // A follow-up in a thread that already has a ticket.
@@ -271,10 +311,19 @@ export class TicketIntakeService {
271
311
  const ack = TICKET_CONSTANTS.REVIEW.ACK_PATTERN.test(text);
272
312
  if (threadTicket) {
273
313
  if (TERMINAL_REQUEST_STATUSES.has(threadTicket.status)) {
274
- // Only a dismissed thread stays quiet; a finished one may open a new ticket.
314
+ // A dismissed thread stays quiet.
275
315
  if (threadTicket.tags.includes(TICKET_CONSTANTS.DISMISSED_TAG)) {
276
316
  return this.ignored(message, 'thread_dismissed');
277
317
  }
318
+ // A recently finished ticket's thread is still its conversation: the
319
+ // owner answering the agent, choosing, parking it — not a new ticket.
320
+ if (this.finishedRecently(threadTicket)) {
321
+ const ask = classifyOwnerMessage(text, { inThread: true });
322
+ if (isGenuinelyNewAsk(text, ask))
323
+ return this.createInThread(all, message, text, threadTicket, ask);
324
+ return this.appendToTicket(threadTicket, message, text);
325
+ }
326
+ // An old thread: a new conversation (it may open a ticket below).
278
327
  }
279
328
  else if (threadTicket.status === 'waiting_confirmation' && this.review) {
280
329
  if (review)
@@ -283,20 +332,17 @@ export class TicketIntakeService {
283
332
  return this.applyReview(threadTicket, { action: 'verify' });
284
333
  // A new ask does not reopen the answered one: it gets its own ticket.
285
334
  const ask = classifyOwnerMessage(text, { inThread: true });
286
- if (ask.verdict === 'new_ask' || ask.verdict === 'question') {
335
+ if (isGenuinelyNewAsk(text, ask))
287
336
  return this.createInThread(all, message, text, threadTicket, ask);
288
- }
289
- // Anything else from the owner: the agent is back on it.
290
- await this.review.reopenOnFollowUp(threadTicket.id);
291
- return this.appendToTicket(threadTicket, message, text);
337
+ // Anything else from the owner that is not 打回 is his OK (2026-09-28).
338
+ return this.acceptOnFollowUp(threadTicket, message, text);
292
339
  }
293
340
  else {
294
- // #827: a thread is not one ticket. A new ask said in it opens its
295
- // own; when unsure the classifier appends (ties go to follow-up).
341
+ // #827: a thread is not one ticket — but only a genuinely new ask
342
+ // opens its own; answers, choices and questions stay in this one.
296
343
  const ask = classifyOwnerMessage(text, { inThread: true });
297
- if (ask.verdict === 'new_ask' || ask.verdict === 'question') {
344
+ if (isGenuinelyNewAsk(text, ask))
298
345
  return this.createInThread(all, message, text, threadTicket, ask);
299
- }
300
346
  return this.appendToTicket(threadTicket, message, text);
301
347
  }
302
348
  }
@@ -313,12 +359,29 @@ export class TicketIntakeService {
313
359
  if (inReview && (review || recent))
314
360
  return this.applyReview(inReview, review ?? { action: 'verify' });
315
361
  }
362
+ // A DM reply right after the conversation's latest ticket is its
363
+ // follow-up — short answers too (「是绿卡」 answers the agent's question).
364
+ if (!threadTicket && text && !TRIVIAL_ACK_PATTERN.test(text)) {
365
+ const recent = this.recentConversationTicket(all, message);
366
+ if (recent && !isGenuinelyNewAsk(text, classifyOwnerMessage(text, { inThread: true }))) {
367
+ if (recent.status === 'waiting_confirmation' && this.review)
368
+ return this.acceptOnFollowUp(recent, message, text);
369
+ return this.appendToTicket(recent, message, text);
370
+ }
371
+ }
316
372
  const trivial = suppressTrivialOrShort(text);
317
373
  if (trivial)
318
374
  return this.ignored(message, trivial);
319
375
  const fileOnly = suppressFileOnly(text, (message.attachments?.length ?? 0) > 0);
320
376
  if (fileOnly)
321
377
  return this.ignored(message, fileOnly);
378
+ if (!threadTicket) {
379
+ // A reply in a thread an agent started (a brief, a question): the owner
380
+ // is answering it. Only a genuinely new ask becomes a ticket.
381
+ if (message.isThreadReply && !isGenuinelyNewAsk(text, classifyOwnerMessage(text, { inThread: true }))) {
382
+ return this.ignored(message, 'thread_reply');
383
+ }
384
+ }
322
385
  // Lazy: v3-data pulls in the pool and storage singletons, which the Slack
323
386
  // modules that import this service must not load at module time.
324
387
  const { classifyIntent } = await import('./v3-data.service.js');
@@ -522,11 +585,75 @@ export class TicketIntakeService {
522
585
  conversationMatches(r, conversation) {
523
586
  return !!r.origin?.threadRef && r.origin.threadRef.startsWith(`${conversation}:`);
524
587
  }
588
+ /**
589
+ * Whether a finished ticket finished recently enough that its thread is
590
+ * still its conversation ({@link TICKET_CONSTANTS.FOLLOW_UP.RECENT_TICKET_MS}).
591
+ *
592
+ * @param ticket - A done or cancelled ticket
593
+ * @returns True when follow-ups in its thread still belong to it
594
+ */
595
+ finishedRecently(ticket) {
596
+ const at = Date.parse(ticket.completedAt ?? ticket.updatedAt);
597
+ return Number.isFinite(at) && this.now().getTime() - at < TICKET_CONSTANTS.FOLLOW_UP.RECENT_TICKET_MS;
598
+ }
599
+ /**
600
+ * In a DM-like conversation (every message top level), the ticket a reply
601
+ * most likely follows up: the conversation's ticket with the latest
602
+ * activity, if that was within {@link TICKET_CONSTANTS.FOLLOW_UP.DM_WINDOW_MS}.
603
+ * Dismissed tickets never take follow-ups.
604
+ *
605
+ * @param all - Every Request
606
+ * @param message - The message
607
+ * @returns The ticket, or null
608
+ */
609
+ recentConversationTicket(all, message) {
610
+ const conversation = message.conversationRef;
611
+ if (!conversation || !TICKET_CONSTANTS.FOLLOW_UP.DM_ORIGINS.includes(message.origin.channel))
612
+ return null;
613
+ const cutoff = this.now().getTime() - TICKET_CONSTANTS.FOLLOW_UP.DM_WINDOW_MS;
614
+ let best = null;
615
+ let bestAt = -Infinity;
616
+ for (const r of all) {
617
+ if (typeof r.ticketNumber !== 'number' || !this.conversationMatches(r, conversation))
618
+ continue;
619
+ if (r.tags.includes(TICKET_CONSTANTS.DISMISSED_TAG) || r.tags.includes(TICKET_CONSTANTS.STALE.TAG))
620
+ continue;
621
+ const at = ticketLastActivity(r);
622
+ if (at >= cutoff && at > bestAt) {
623
+ best = r;
624
+ bestAt = at;
625
+ }
626
+ }
627
+ return best;
628
+ }
629
+ /**
630
+ * An owner follow-up on a 待验收 ticket that is not 打回 (2026-09-28): it is
631
+ * his OK. The ticket is accepted and the message kept in its discussion;
632
+ * when it cannot be accepted yet (live work), the agent is back on it.
633
+ *
634
+ * @param ticket - The 待验收 ticket
635
+ * @param message - The follow-up
636
+ * @param text - Trimmed text
637
+ * @returns `verified`, or `appended` when it could not be accepted
638
+ */
639
+ async acceptOnFollowUp(ticket, message, text) {
640
+ const review = this.review;
641
+ if (!review)
642
+ return this.appendToTicket(ticket, message, text);
643
+ const result = await review.verify(ticket.id);
644
+ if (!result.ok) {
645
+ const reopened = (await review.reopenOnFollowUp(ticket.id)) ?? ticket;
646
+ return this.appendToTicket(reopened, message, text);
647
+ }
648
+ const appended = await this.appendToTicket(result.ticket ?? ticket, message, text);
649
+ return { action: 'verified', ticket: appended.action === 'appended' ? appended.ticket : ticket };
650
+ }
525
651
  /**
526
652
  * Append a follow-up to a ticket's discussion.
527
653
  *
528
- * Trivial follow-ups ("好的") are not written, but the message still belongs
529
- * to the ticket (so the delivered copy carries its marker).
654
+ * Pure acknowledgements ("好的", "thanks") are not written, but the message
655
+ * still belongs to the ticket (so the delivered copy carries its marker).
656
+ * Short answers are written: 「是绿卡」 is what the agent asked for.
530
657
  *
531
658
  * @param ticket - The open ticket
532
659
  * @param message - The follow-up
@@ -534,8 +661,9 @@ export class TicketIntakeService {
534
661
  * @returns `appended` outcome
535
662
  */
536
663
  async appendToTicket(ticket, message, text) {
537
- if (suppressTrivialOrShort(text) || text.length === 0)
664
+ if (text.length === 0 || TRIVIAL_ACK_PATTERN.test(text) || suppressFileOnly(text, (message.attachments?.length ?? 0) > 0)) {
538
665
  return { action: 'appended', ticket };
666
+ }
539
667
  const discussion = [
540
668
  ...(ticket.discussion ?? []),
541
669
  {
@@ -831,9 +959,7 @@ export class TicketIntakeService {
831
959
  ? null
832
960
  : r.acceptedBy ?? (r.tags.includes(TICKET_CONSTANTS.REVIEW.AUTO_ACCEPTED_TAG) ? 'silence' : null),
833
961
  autoAcceptAt: r.status === 'waiting_confirmation' && r.submittedAt && ticketNeedsReview(r)
834
- ? new Date(Date.parse(r.lastNudgeAt ?? r.submittedAt) +
835
- TICKET_CONSTANTS.REVIEW.NUDGE_AFTER_MS *
836
- (TICKET_CONSTANTS.REVIEW.MAX_NUDGES - (r.nudgeCount ?? 0) + 1)).toISOString()
962
+ ? new Date(Date.parse(r.submittedAt) + TICKET_CONSTANTS.REVIEW.AUTO_ACCEPT_MS).toISOString()
837
963
  : null,
838
964
  };
839
965
  }