@fastagent-sh/fastagent 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +258 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. package/dist/workspace.js +0 -45
@@ -26,7 +26,7 @@ function splitPlain(text: string): string[] {
26
26
  return parts;
27
27
  }
28
28
 
29
- // Standalone copy of the channel transport's discipline: an upload timeout so a wedged connection
29
+ // Embedded copy of the channel transport's discipline: an upload timeout so a wedged connection
30
30
  // can't hang the tool call (and the turn), named errors, and success gated on the body's own ok.
31
31
  // Deliberately NO 429 retry — a tool error goes back to the agent, which can decide to retry;
32
32
  // fail-fast beats a silently sleeping tool.
@@ -60,8 +60,11 @@ export default defineTool({
60
60
  description:
61
61
  "Send to a Telegram chat: a text message (`text` — long text is split into multiple messages " +
62
62
  "automatically), or a local file (`path` — a document, or a photo if it is an image). Exactly one " +
63
- "of text/path. In a chat turn take chatId from the [telegram: chat …] context line; in a " +
64
- "scheduled/woken turn (no context line) the chat id must come from your instruction.",
63
+ "of text/path. Use it for a turn NO channel is carrying a scheduled or self-scheduled (wake) " +
64
+ "turn or to reach a chat OTHER than the one you are answering. In a normal chat turn the channel " +
65
+ "already delivers your reply, so do NOT call this to answer (it would send it twice). chatId comes " +
66
+ "from the [telegram: chat …] context line in a chat turn; a scheduled/woken turn has no context " +
67
+ "line, so name the destination in your instruction.",
65
68
  input: z.object({
66
69
  chatId: z.union([z.string(), z.number()]).describe("target chat id"),
67
70
  text: z.string().optional().describe("message text to send"),
@@ -32,6 +32,7 @@ import { collectAttachments, createContextBuffer } from "./context-buffer.js";
32
32
  import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, } from "./parse.js";
33
33
  import { defaultErrorMessage, streamReply } from "./preview.js";
34
34
  import { ensureStateHome } from "../state.js";
35
+ import { dispatchStop } from "../stop-command.js";
35
36
  import { callApi, editMessageText, sendMessage } from "./telegram-api.js";
36
37
  import { createTurnQueue } from "../turn-queue.js";
37
38
  import { createTurnStore } from "./turn-store.js";
@@ -67,15 +68,14 @@ function tokenMatches(header, secret) {
67
68
  * share the first's turn-store/context-buffer. One telegram instance per workspace (single-process).
68
69
  */
69
70
  export function telegramChannel({ secretToken, botToken, route, onError, botUsername, apiBaseUrl = "https://api.telegram.org", }) {
70
- // Both are mandatory: an unset secret_token would accept forged updates (the endpoint is public);
71
- // the bot token is required to send the reply. Fail at construction (startup), not silently.
72
- if (!secretToken) {
73
- throw new Error("telegramChannel requires a non-empty secretToken (the webhook secret_token; an unset one accepts forged updates)");
74
- }
75
- if (!botToken) {
76
- throw new Error("telegramChannel requires a non-empty botToken (used to send the agent's reply)");
77
- }
78
- return ({ agent, stateRoot }) => {
71
+ return ({ agent, stateRoot, control }) => {
72
+ // Validate at activation so deploy may inspect the module shape before secrets are provisioned.
73
+ if (!secretToken) {
74
+ throw new Error("telegramChannel requires a non-empty secretToken (the webhook secret_token; an unset one accepts forged updates)");
75
+ }
76
+ if (!botToken) {
77
+ throw new Error("telegramChannel requires a non-empty botToken (used to send the agent's reply)");
78
+ }
79
79
  const formatError = onError ?? defaultErrorMessage;
80
80
  // One getMe at startup: the bot's @username (for the default route's group @mention summon, only when
81
81
  // not supplied) and the group-privacy flag — privacy mode off is required to receive the un-summoned
@@ -109,7 +109,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
109
109
  throw new Error(`telegramChannel requires an absolute ctx.stateRoot, got "${stateRoot}"`);
110
110
  }
111
111
  const stateHome = join(stateRoot, "channels", "telegram");
112
- ensureStateHome(stateHome); // create + self-ignore — buffers/files may carry chat content
112
+ ensureStateHome(stateHome); // buffers/files may carry chat content; the agent .gitignore covers .state/
113
113
  const buffer = createContextBuffer(join(stateHome, "buffers.json"));
114
114
  // Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash
115
115
  // leaves it for replay on the next start. See turn-store.ts for the at-least-once semantics.
@@ -323,6 +323,20 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
323
323
  // that explicitly returns the same chat/thread still quotes.
324
324
  const threadId = r.threadId ?? m.message_thread_id;
325
325
  const sameTarget = String(chatId) === String(m.chat.id) && threadId === m.message_thread_id;
326
+ // Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the
327
+ // run it stops. `/stop@otherbot` is not ours; a bare `/stop` always is. Awaited before the ACK:
328
+ // dispatch + one sendMessage is fast, and a delivery failure logs instead of failing the webhook.
329
+ const stopMatch = /^\/stop(?:@([A-Za-z0-9_]+))?$/i.exec(messageText(m).trim());
330
+ if (stopMatch && (!stopMatch[1] || stopMatch[1].toLowerCase() === mentionName?.toLowerCase())) {
331
+ const feedback = await dispatchStop(control, session, "[telegram]");
332
+ const target = {
333
+ chatId,
334
+ threadId,
335
+ replyTo: m.chat.type !== "private" && sameTarget ? m.message_id : undefined,
336
+ };
337
+ await sendMessage(apiBaseUrl, botToken, target, feedback, { html: false }).catch((e) => log.warn(`[telegram] stop feedback failed: ${String(e)}`));
338
+ return new Response(null, { status: 200 });
339
+ }
326
340
  const baseText = r.text ?? telegramEnvelope(m);
327
341
  const imageFileIds = extractImages(m);
328
342
  const fileIds = extractFiles(m);
@@ -1,6 +1,20 @@
1
- /** Pure Unicode-safe text slicing helpers shared by Feishu/Lark rendering paths. JavaScript string
1
+ /** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). JavaScript string
2
2
  * indexes are UTF-16 code units, so direct `slice()` can tear a surrogate pair and send replacement
3
3
  * characters after JSON/UTF-8 encoding. These helpers only cut at Unicode code-point boundaries. */
4
+ /**
5
+ * How much of a replied-to message is quoted back into a prompt.
6
+ *
7
+ * A referent is the exact text the asker is pointing AT, not a summary of it, so the bound is a
8
+ * fidelity bound: it must clear the largest message a chat platform will accept (Telegram's 4096 is
9
+ * the tightest of ours) or a perfectly legal message loses its tail and the agent answers about text
10
+ * it cannot see — silently. Past that point it is only a guard against a pathological message on a
11
+ * platform with no practical cap. One constant for every channel: the failure this replaces was two
12
+ * channels picking their own number and drifting 14x apart.
13
+ *
14
+ * Distinct from a context-buffer line (see BUFFER_LINE_MAX_CHARS), which is a digest competing for a
15
+ * shared budget — different job, different unit, must not share a number.
16
+ */
17
+ export declare const REFERENT_MAX_CODE_POINTS = 4096;
4
18
  /** Take at most `maxPoints` Unicode code points from the start, without adding a marker. */
5
19
  export declare function codePointPrefix(text: string, maxPoints: number): string;
6
20
  /** Ellipsize from the right while keeping the result within `maxPoints` Unicode code points. */
@@ -1,6 +1,20 @@
1
- /** Pure Unicode-safe text slicing helpers shared by Feishu/Lark rendering paths. JavaScript string
1
+ /** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). JavaScript string
2
2
  * indexes are UTF-16 code units, so direct `slice()` can tear a surrogate pair and send replacement
3
3
  * characters after JSON/UTF-8 encoding. These helpers only cut at Unicode code-point boundaries. */
4
+ /**
5
+ * How much of a replied-to message is quoted back into a prompt.
6
+ *
7
+ * A referent is the exact text the asker is pointing AT, not a summary of it, so the bound is a
8
+ * fidelity bound: it must clear the largest message a chat platform will accept (Telegram's 4096 is
9
+ * the tightest of ours) or a perfectly legal message loses its tail and the agent answers about text
10
+ * it cannot see — silently. Past that point it is only a guard against a pathological message on a
11
+ * platform with no practical cap. One constant for every channel: the failure this replaces was two
12
+ * channels picking their own number and drifting 14x apart.
13
+ *
14
+ * Distinct from a context-buffer line (see BUFFER_LINE_MAX_CHARS), which is a digest competing for a
15
+ * shared budget — different job, different unit, must not share a number.
16
+ */
17
+ export const REFERENT_MAX_CODE_POINTS = 4096;
4
18
  /** Take at most `maxPoints` Unicode code points from the start, without adding a marker. */
5
19
  export function codePointPrefix(text, maxPoints) {
6
20
  if (maxPoints <= 0)
@@ -0,0 +1,21 @@
1
+ export interface ThreadParticipants {
2
+ /**
3
+ * The participant model's summon rule (docs/design/participant-model.md §3): does a bare message in
4
+ * this thread address the agent? True where it takes part AND no second human has been heard. Lives
5
+ * here rather than in each channel because it is one rule over one store — `<= 1` is an easy edge to
6
+ * get wrong twice, and "a second human restores the mention requirement" must have one place to change.
7
+ */
8
+ admitsBareMessage(key: string): boolean;
9
+ /**
10
+ * Merge in what was just heard. Idempotent; a failed write is a warning, never a failed delivery.
11
+ *
12
+ * The parameter only admits values the store can honour: observations accumulate, so `agentSpoke`
13
+ * can be set but never cleared and `humans` unions. Passing `false` would compile and do nothing,
14
+ * so the type refuses it — "never shed" is an invariant, not a convention.
15
+ */
16
+ merge(key: string, heard: {
17
+ humans?: string[];
18
+ agentSpoke?: true;
19
+ }): void;
20
+ }
21
+ export declare function createThreadParticipants(path: string, label: string): ThreadParticipants;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * SHARED: who the agent has HEARD in a group thread — the sole input to the participant model's summon
3
+ * rule. **The derivation lives in docs/design/participant-model.md §3** (why the rule is defined over
4
+ * observation rather than the thread's true membership, what the weaker claim costs in both
5
+ * directions, and why recording is never gated on configuration). Repeating it here would mean two
6
+ * copies to keep true; what follows is only what a caller of this module must not get wrong.
7
+ *
8
+ * - **The key MUST be the string the session uses.** A record here is a claim about that session's
9
+ * memory, so the two cannot be keyed independently — including the channel brand
10
+ * (`feishu:<chat>:<thread>`, `slack:<team>:<channel>:<thread_ts>`). This file is already per-channel
11
+ * and would not need the prefix, but SESSION ids share one namespace across every channel in a
12
+ * deployment, so dropping it would key participation to a session that is not the one that answered.
13
+ * - **Write both halves under one condition**, and gate that condition on STRUCTURAL facts only (is
14
+ * this a group? a thread? a human speaking?) — never on configuration, which changes while records
15
+ * outlive the change.
16
+ * - **Observations only accumulate.** Nothing here sheds: no platform signals that someone stopped
17
+ * taking part, and the error directions are not symmetric — over-counting humans makes the agent ask
18
+ * to be named, under-counting makes it speak into a crowd.
19
+ * - Keyed by `thread_id`, never a reply-chain root: Feishu's `root_id` moves with the chain, so it
20
+ * cannot identify a side conversation at all.
21
+ */
22
+ import { log } from "../log.js";
23
+ import { loadStateFile, saveStateFile } from "./state.js";
24
+ /** Cap on remembered threads. Losing one costs a mention to re-enter that thread, so an unbounded file
25
+ * buys little — and a merge that carries new information rewrites the whole map synchronously, so the
26
+ * map's size is the cost of every such write. What keeps that bounded is that only NEW information
27
+ * writes at all (a repeat speaker, or any message once MAX_HUMANS is reached, returns before
28
+ * persisting), and that the map stays small enough for state.ts's premise to hold — these writes land
29
+ * synchronously on the acceptance path, so the whole file is the cost of each one. A thousand records
30
+ * is tens of KB, and the records that matter (threads the agent takes part in) are far fewer than
31
+ * that; the dominant traffic is bystanders, which is what the eviction policy below is aimed at.
32
+ *
33
+ * Eviction prefers BYSTANDER threads — ones the agent has only listened to. They are written on the
34
+ * same path and vastly outnumber the rest (every thread in every visible channel), yet losing one
35
+ * costs nothing: the summon rule refuses a thread the agent has not spoken in anyway, so the record
36
+ * would have to be rebuilt by the mention that admits it. Evicting purely by age would let this
37
+ * traffic push out the threads the agent is actively serving, silently reverting them to
38
+ * mention-only. This is also what makes it safe to record threads no rule currently reads (those
39
+ * behind a custom route, or under a posture whose summon rule is off — see the header). */
40
+ const MAX_THREADS = 1000;
41
+ /** Cap on remembered humans per thread. The rule only asks "have I heard a second one?", so two is
42
+ * already the whole answer and anything beyond it is weight nothing reads. */
43
+ const MAX_HUMANS = 2;
44
+ function isStoredParticipation(value) {
45
+ const record = value;
46
+ return (Array.isArray(record?.humans) &&
47
+ record.humans.every((human) => typeof human === "string") &&
48
+ typeof record.agentSpoke === "boolean");
49
+ }
50
+ export function createThreadParticipants(path, label) {
51
+ const raw = loadStateFile(path);
52
+ const records = new Map();
53
+ if (raw !== undefined) {
54
+ if (typeof raw === "object" &&
55
+ raw !== null &&
56
+ !Array.isArray(raw) &&
57
+ Object.values(raw).every(isStoredParticipation)) {
58
+ for (const [key, record] of Object.entries(raw)) {
59
+ records.set(key, { humans: record.humans, agentSpoke: record.agentSpoke });
60
+ }
61
+ }
62
+ else {
63
+ log.warn(`${label} unexpected shape in ${path} — starting with no thread participation`);
64
+ }
65
+ }
66
+ return {
67
+ admitsBareMessage(key) {
68
+ const heard = records.get(key);
69
+ return heard?.agentSpoke === true && heard.humans.length <= 1;
70
+ },
71
+ merge(key, heard) {
72
+ const previous = records.get(key);
73
+ const humans = new Set(previous?.humans ?? []);
74
+ for (const human of heard.humans ?? []) {
75
+ if (humans.size >= MAX_HUMANS)
76
+ break;
77
+ humans.add(human);
78
+ }
79
+ const next = {
80
+ humans: [...humans],
81
+ agentSpoke: (previous?.agentSpoke ?? false) || (heard.agentSpoke ?? false),
82
+ };
83
+ // `humans` starts from `previous` and only grows, so equal size IS set equality here.
84
+ const unchanged = previous !== undefined &&
85
+ previous.agentSpoke === next.agentSpoke &&
86
+ previous.humans.length === next.humans.length;
87
+ // Re-insert so insertion order is "least recently TOUCHED first" — including when nothing
88
+ // changed. A thread in its steady state (the agent answers, the same person keeps talking) stops
89
+ // carrying new information and would otherwise never refresh its position again, leaving the
90
+ // thread being served right now at the head of the eviction order.
91
+ //
92
+ // PROCESS-LOCAL: a touch alone never writes, and such a thread has nothing left to write (it has
93
+ // reached MAX_HUMANS), so the refreshed order survives only until restart. That is the right
94
+ // trade for a cache — persisting recency would mean a whole-map write per message — and the
95
+ // consequence is bounded: after a restart, eviction order among participant threads is the order
96
+ // they last carried new information.
97
+ records.delete(key);
98
+ records.set(key, next);
99
+ if (unchanged)
100
+ return;
101
+ while (records.size > MAX_THREADS) {
102
+ // Oldest bystander first; only when every record is a thread the agent takes part in does age
103
+ // alone decide. NEVER the key just merged: it is the most recently touched record, so evicting
104
+ // it contradicts the recency policy re-established above — and worse, a channel that records a
105
+ // thread's humans and its own participation in two steps would then write the second half onto
106
+ // an empty record, producing "answered here, heard nobody" and a permanent bare-message admit.
107
+ let evict;
108
+ for (const [candidate, record] of records) {
109
+ if (candidate === key)
110
+ continue;
111
+ if (!record.agentSpoke) {
112
+ evict = candidate;
113
+ break;
114
+ }
115
+ evict ??= candidate;
116
+ }
117
+ if (evict === undefined)
118
+ break;
119
+ records.delete(evict);
120
+ }
121
+ try {
122
+ saveStateFile(path, Object.fromEntries(records));
123
+ }
124
+ catch (error) {
125
+ // Cache only: memory stays correct for this process, and the whole map is rewritten on the
126
+ // next successful merge — so a failed write costs durability only until then, and a thread
127
+ // whose record is lost simply needs a mention to re-enter.
128
+ log.warn(`${label} could not persist thread participation ${path}: ${String(error)}`);
129
+ }
130
+ },
131
+ };
132
+ }
@@ -5,12 +5,13 @@
5
5
  * instead of colliding on the lease and being dropped as "busy").
6
6
  *
7
7
  * Channel-neutral (records are opaque beyond a `session` key), shared by the stateful chat channels
8
- * (telegram, Feishu; Lark reuses Feishu). Durability is layered ON TOP by the caller: turn-store.ts
8
+ * (Telegram, Feishu/Lark, Slack). Durability is layered ON TOP by the caller: turn-store.ts
9
9
  * persists an accepted turn's intent pre-ACK and replays a crash-surviving one on the next start (L1,
10
10
  * process-crash recovery, at-least-once). Exactly-once / deterministic step-replay (L2) is the K-axis
11
11
  * backend — an external queue with distributed locking (SPEC §11) — not this in-memory queue.
12
12
  */
13
13
  import { log } from "../log.js";
14
+ import { beginWork } from "./busy.js";
14
15
  export function createTurnQueue(opts) {
15
16
  const { label, run, onQueuedBehind } = opts;
16
17
  // Per-session serial chains: a second turn for the same session waits its turn (FIFO) instead of
@@ -20,6 +21,11 @@ export function createTurnQueue(opts) {
20
21
  accept(rec) {
21
22
  if (chains.has(rec.session))
22
23
  onQueuedBehind?.(rec);
24
+ // Process-wide busy signal (busy.ts): an accepted turn counts as in-flight work from acceptance
25
+ // until its run settles — serving surfaces that must not idle mid-turn (the AgentCore adapter's
26
+ // /ping) read it. Counted here, not in run(), so a QUEUED turn (accepted, waiting on the chain)
27
+ // already reads as busy.
28
+ const workDone = beginWork();
23
29
  const prev = chains.get(rec.session) ?? Promise.resolve();
24
30
  const task = async () => {
25
31
  try {
@@ -32,6 +38,7 @@ export function createTurnQueue(opts) {
32
38
  const next = prev.then(task, task); // run after this session's previous turn, in arrival order
33
39
  chains.set(rec.session, next);
34
40
  void next.finally(() => {
41
+ workDone();
35
42
  if (chains.get(rec.session) === next)
36
43
  chains.delete(rec.session); // drop the entry when drained
37
44
  });
@@ -9,7 +9,7 @@ export interface TurnRecordBase {
9
9
  attempts: number;
10
10
  }
11
11
  export interface TurnStore<T extends TurnRecordBase> {
12
- /** Persist an accepted turn before the ACK. A failed write throws (→ webhook 500, the platform redelivers). */
12
+ /** Persist an accepted turn before the ACK. A failed write throws (→ HTTP response / WS ACK 500, so the platform redelivers). */
13
13
  add(rec: T): void;
14
14
  /** Remove a finished turn. Post-ACK: a failed write is logged, never thrown (must not abort delivery). */
15
15
  remove(id: string): void;
@@ -31,8 +31,7 @@
31
31
  *
32
32
  * Channel-neutral, generic over the record shape: the channel supplies its own record type (everything
33
33
  * its runner needs to re-execute the turn), the shape validator (state files are an IO boundary), the
34
- * arrival ordering for recovery, and its log label. telegram/turn-store.ts and the Feishu channel are the
35
- * two consumers.
34
+ * arrival ordering for recovery, and its log label. Telegram, Feishu/Lark, and Slack consume it.
36
35
  *
37
36
  * ponytail: at-least-once with a per-turn EXECUTION ceiling. A poison turn that deterministically
38
37
  * crashes the process would replay forever under a container restart policy. The counter is bumped at
@@ -84,7 +83,7 @@ export function createTurnStore(path, opts) {
84
83
  return;
85
84
  turns.set(rec.id, rec);
86
85
  try {
87
- persist(); // pre-ACK: a throw becomes the webhook's 500 and the platform redelivers
86
+ persist(); // pre-ACK: the transport maps this throw to HTTP/WS 500, so the platform redelivers
88
87
  }
89
88
  catch (e) {
90
89
  // Roll the memory back so it matches disk (mirrors context-buffer.push): otherwise the phantom
@@ -0,0 +1,30 @@
1
+ import { type FeishuGroupBehavior, type FeishuSubscriptionMode } from "../channels/feishu/setup-mode.ts";
2
+ import { type FeishuApi } from "../channels/feishu/feishu-api.ts";
3
+ import { type GroupBehaviorChoice } from "../scaffold/add-channel.ts";
4
+ export interface GroupBehaviorSetup {
5
+ /** Safe to proceed to version publishing now; false means Permissions still needs manual/admin work. */
6
+ publishReady: boolean;
7
+ }
8
+ export declare function configureGroupBehavior(input: {
9
+ kind: "feishu" | "lark";
10
+ appId: string;
11
+ apiBase: string;
12
+ api: Pick<FeishuApi, "listAppScopes" | "addAppScopes">;
13
+ behavior: FeishuGroupBehavior;
14
+ /** Whether the author chose the behavior (flag or prompt). A defaulted "context" inspects and
15
+ * reports only — it must never PATCH the sensitive scope into the app draft. */
16
+ explicit: boolean;
17
+ note?: (message: string) => void;
18
+ openUrl?: (url: string) => void;
19
+ }): Promise<GroupBehaviorSetup>;
20
+ /**
21
+ * Create or resume the platform app behind `add feishu` / `add lark`. `target` is the AGENT DIR;
22
+ * credentials land in its `.env` — whose real path is {@link dotEnvPath}, printed rather than spelled,
23
+ * because `FASTAGENT_SECRETS_DIR` moves it and a hardcoded `.secrets/.env` would name a file this run
24
+ * did not write. Returns credentials for the
25
+ * caller's generic .env write (the guided Lark path), or undefined when nothing remains to write —
26
+ * the feishu path persists its own two credential stages internally (the App ID/Secret boundary is
27
+ * irreversible and must not wait for the caller). Throws on refusal (a non-interactive lark run);
28
+ * the caller surfaces that as a startup failure.
29
+ */
30
+ export declare function onboardFeishuCloudApp(target: string, kind: "feishu" | "lark", ingress?: FeishuSubscriptionMode, groupBehavior?: GroupBehaviorChoice): Promise<Record<string, string> | undefined>;