mixdog 1.3.3 → 1.3.5

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 (210) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +3 -2
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/agent-room-host.mjs +1 -1
  128. package/src/runtime/shared/bot-hops.mjs +62 -2
  129. package/src/runtime/shared/config.mjs +49 -22
  130. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  131. package/src/runtime/shared/connector-catalog.mjs +16 -8
  132. package/src/runtime/shared/developer-options.mjs +32 -1
  133. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  134. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  135. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  136. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  137. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  138. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  139. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  140. package/src/runtime/shared/provider-accounts.mjs +224 -58
  141. package/src/runtime/shared/provider-api-key.mjs +19 -8
  142. package/src/runtime/shared/routine-trigger.mjs +42 -0
  143. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  145. package/src/runtime/shared/user-data-guard.mjs +30 -2
  146. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  147. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  148. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  149. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  150. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  151. package/src/session-runtime/boot/begin.mjs +3 -2
  152. package/src/session-runtime/boot/tools.mjs +4 -0
  153. package/src/session-runtime/bot-chain.mjs +32 -0
  154. package/src/session-runtime/bot-status.mjs +7 -4
  155. package/src/session-runtime/channel-config-api.mjs +63 -17
  156. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  157. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  158. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  159. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  160. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  162. package/src/session-runtime/owned-automation-run.mjs +33 -12
  163. package/src/session-runtime/provider-auth/login.mjs +7 -0
  164. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  165. package/src/session-runtime/routine-home-turn.mjs +33 -6
  166. package/src/session-runtime/schedule-session-run.mjs +7 -3
  167. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  168. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  169. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
  170. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  171. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  172. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  173. package/src/session-runtime/services/channel-admin.mjs +66 -18
  174. package/src/session-runtime/services/channel-worker.mjs +1 -1
  175. package/src/session-runtime/services/provider-admin.mjs +128 -25
  176. package/src/session-runtime/services/schedule-access.mjs +14 -0
  177. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  178. package/src/session-runtime/session-create/session-options.mjs +7 -4
  179. package/src/session-runtime/settings-system-api.mjs +16 -1
  180. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  181. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  182. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  183. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  184. package/src/session-runtime/task-session-route.mjs +99 -0
  185. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  186. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  187. package/src/session-runtime/tool-surface.mjs +4 -0
  188. package/src/session-runtime/usage-stats-api.mjs +1 -1
  189. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  190. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  191. package/src/session-runtime/workspace/transitions.mjs +10 -2
  192. package/src/standalone/daemon-session-control.mjs +88 -20
  193. package/src/standalone/daemon.mjs +8 -0
  194. package/src/standalone/session-protocol.mjs +5 -0
  195. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  196. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  197. package/src/standalone/session-service.mjs +13 -0
  198. package/src/standalone/session-tool-output.mjs +8 -5
  199. package/src/tui/app/model-options.mjs +2 -0
  200. package/src/tui/dist/index.mjs +335 -172
  201. package/src/tui/session/notice-surface.mjs +17 -1
  202. package/src/tui/session/session-api/integrations.mjs +1 -0
  203. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  204. package/src/tui/session/session-api/settings.mjs +14 -2
  205. package/src/tui/session/turn-stream.mjs +17 -1
  206. package/src/tui/session/turn.mjs +22 -2
  207. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  208. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  209. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  210. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -0,0 +1,133 @@
1
+ // react: a bot's emoji reaction on the user's message, shown under the user's
2
+ // bubble the way a messenger shows one. Offered only in a bot's own chat with
3
+ // the user (its home chat or its seat in a group room) while the Reactions
4
+ // setting is on (session-tool-surface.mjs, builtin-features.mjs).
5
+ export const REACTION_TOOL_NAME = 'react';
6
+
7
+ /** The reactions a bot can leave, in the order the schema lists them. */
8
+ export const REACTION_EMOJI = Object.freeze(['❤️', '👍', '✔️', '😆', '😮', '😢', '🙏']);
9
+ const MESSAGES_BACK_MAX = 20;
10
+
11
+ export const REACTION_TOOL_DEFS = Object.freeze([
12
+ {
13
+ name: REACTION_TOOL_NAME,
14
+ title: 'React',
15
+ trigger: "Leave one emoji reaction on the user's message, as a chat app shows one under it.",
16
+ description:
17
+ "Leave one emoji reaction under the user's message. Two uses only. Instead of a reply: when the message needs no words back (ok, got it, agreement, thanks, a done update), react alone with no reply text, when the reaction fully answers, instead of a filler reply like 'sounds good'. As emphasis: on clear good or sad news or something truly funny, react and still reply as the message needs; keep this rare, about once per 5-10 exchanges. Otherwise none: never react to questions or requests needing real content, never on two messages in a row, never as a status signal, and never mention or explain a reaction. ❤️ warmth or good news, 👍 agreement or ok, ✔️ done or noted, 😆 funny, 😮 surprising, 😢 sad, 🙏 thanks. In a group room, react only to messages for you, never repeating one another member left. Reacting again replaces yours.",
18
+ annotations: {
19
+ title: 'React',
20
+ readOnlyHint: false,
21
+ destructiveHint: false,
22
+ idempotentHint: true,
23
+ openWorldHint: false,
24
+ agentHidden: true,
25
+ },
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ emoji: { type: 'string', description: `One of ${REACTION_EMOJI.join(' ')}.` },
30
+ messages_back: {
31
+ type: 'integer',
32
+ minimum: 0,
33
+ maximum: MESSAGES_BACK_MAX,
34
+ description: "0 or omitted: the user's latest message; 1: the one before it, and so on.",
35
+ },
36
+ remove: { type: 'boolean', description: 'Remove your reaction instead.' },
37
+ },
38
+ additionalProperties: false,
39
+ },
40
+ },
41
+ ]);
42
+
43
+ const bare = (text) => text.replace(/\uFE0F/g, '');
44
+
45
+ /** The reaction `value` names (a missing or extra U+FE0F tolerated), or ''. */
46
+ export function reactionEmoji(value) {
47
+ const text = typeof value === 'string' ? bare(value.trim()) : '';
48
+ return (text && REACTION_EMOJI.find((emoji) => bare(emoji) === text)) || '';
49
+ }
50
+
51
+ /** `{ emoji, messagesBack }` of a react call (`emoji` '' removes); throws on
52
+ * anything else. */
53
+ export function validateReactionArgs(args) {
54
+ const input = args && typeof args === 'object' ? args : {};
55
+ const back = input.messages_back ?? 0;
56
+ if (!Number.isInteger(back) || back < 0 || back > MESSAGES_BACK_MAX) {
57
+ throw new Error(`react: messages_back is a whole number from 0 to ${MESSAGES_BACK_MAX}`);
58
+ }
59
+ if (input.remove === true) return { emoji: '', messagesBack: back };
60
+ const emoji = reactionEmoji(input.emoji);
61
+ if (!emoji) throw new Error(`react: emoji must be exactly one of ${REACTION_EMOJI.join(' ')}`);
62
+ return { emoji, messagesBack: back };
63
+ }
64
+
65
+ /** Whether a provider tool call (direct, or bridged through call_tool) is a react call. */
66
+ function isReactionToolCall(call) {
67
+ const name = call?.name ?? call?.function?.name;
68
+ if (name === REACTION_TOOL_NAME) return true;
69
+ return name === 'call_tool' && call?.arguments?.name === REACTION_TOOL_NAME;
70
+ }
71
+
72
+ /** The ids of a batch's react calls. */
73
+ export function reactionCallIds(calls) {
74
+ return new Set(
75
+ (calls || [])
76
+ .filter(isReactionToolCall)
77
+ .map((call) => call?.id)
78
+ .filter(Boolean)
79
+ );
80
+ }
81
+
82
+ // What each successful react call actually hit, keyed by session and tool
83
+ // call id: the tool result reaches the loop as plain text (internal-tools.mjs
84
+ // normalizes every handler return to a string), so this is the result's
85
+ // metadata side channel. `{ messageId, room }`: the id of the message
86
+ // reactToMessage reacted on, and whether that log is a group room's (a
87
+ // separate transcript from the worker's own).
88
+ //
89
+ // A record belongs to the loop of its session: the batch that ran the call
90
+ // consumes it (takeReactionTarget), agentLoop clears the session's leftovers
91
+ // whenever it ends, success or not (clearReactionTargets), and the cap bounds
92
+ // the map even if neither ever ran.
93
+ const REACTION_TARGET_CAP = 256;
94
+ const reactionTargets = new Map();
95
+ const targetKey = (sessionId, toolCallId) => `${sessionId}\u0000${toolCallId}`;
96
+
97
+ /** Records the message a successful react call landed on. */
98
+ export function noteReactionTarget(sessionId, toolCallId, target) {
99
+ if (!sessionId || !toolCallId || !target?.messageId) return;
100
+ reactionTargets.set(targetKey(sessionId, toolCallId), target);
101
+ if (reactionTargets.size > REACTION_TARGET_CAP) reactionTargets.delete(reactionTargets.keys().next().value);
102
+ }
103
+
104
+ /** Drops every record of `sessionId`: its loop has ended. */
105
+ export function clearReactionTargets(sessionId) {
106
+ const prefix = `${sessionId}\u0000`;
107
+ for (const key of reactionTargets.keys()) if (key.startsWith(prefix)) reactionTargets.delete(key);
108
+ }
109
+
110
+ /** How many records are held (tests). */
111
+ export function reactionTargetCount() {
112
+ return reactionTargets.size;
113
+ }
114
+
115
+ /** The recorded target of a react call, consumed; null when it recorded none. */
116
+ export function takeReactionTarget(sessionId, toolCallId) {
117
+ const key = targetKey(sessionId, toolCallId);
118
+ const target = reactionTargets.get(key) ?? null;
119
+ reactionTargets.delete(key);
120
+ return target;
121
+ }
122
+
123
+ /** Whether a tool result reports one of `ids` (reactionCallIds) as executed
124
+ * and succeeded: a rejected, denied, skipped or failed call is not a reaction,
125
+ * and neither is an early UI-only notify that precedes the real result. */
126
+ export function isReactionSuccess(message, ids) {
127
+ return (
128
+ message?.role === 'tool' &&
129
+ message.__earlyNotify !== true &&
130
+ message.toolKind === 'normal' &&
131
+ ids.has(message.toolCallId)
132
+ );
133
+ }
@@ -77,6 +77,9 @@ export const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
77
77
  // Always on the Lead surface (native and bridge alike): a question is an
78
78
  // optional move the model skips when it costs a load or call_tool hop.
79
79
  'ask_user',
80
+ // A bot's reaction, likewise: only a bot's chat with the user has it at all
81
+ // (session-tool-surface.mjs reactionDisabled).
82
+ 'react',
80
83
  'Skill',
81
84
  'load_tool',
82
85
  ]);
@@ -73,7 +73,8 @@ export const SESSION_TOOL = {
73
73
  openWorldHint: false,
74
74
  agentHidden: true,
75
75
  },
76
- trigger: 'Session lookup/control, bot status, agent task sessions and their initial location, title, compact, fork, Project.',
76
+ trigger:
77
+ 'Session lookup/control, bot status, agent task sessions and their initial location, title, compact, fork, Project.',
77
78
  description:
78
79
  'Inspect sessions or control another by exact ID, never a guessed one. send acknowledges queue intake, not completion: promise a report of it only when its result has reportsBack: true. create returns status: started — started, not done yet; it reports its final answer, after the agents it started finish, unless its result says reportsBack: false. reportsBack: false carries reportsBackReason, the reason to tell the user. Cancel/delete require the user’s request. Other actions affect only the current session. A new session’s initial location (Project folder branch or its own worktree) is set here, with create’s worktree; worktree operations on existing sessions (enter, return, merge, remove, list, prune) use the git tool’s action:"worktree". Pass files on with attachments; a reported turn returns the files it produced as attachments.',
79
80
  inputSchema: {
@@ -97,11 +98,20 @@ export const SESSION_TOOL = {
97
98
  'list-projects',
98
99
  'favorite',
99
100
  'archive',
101
+ 'suggest',
100
102
  ],
101
103
  description:
102
- 'status defaults to current; list pages sessions, without sub-agent or archived ones unless include_agents/include_archived; bots shows each bot’s activity, work, approvals, routines and attention flags, or with session_id (a bot id or its chat) its routines and recent sessions; read returns latest items; send queues prompt (reportsBack: the turn result reports back here); cancel stops the turn; delete removes the session. create (bot conversations) starts a visible agent task session from brief (started, not done yet); unknown Project: list-projects first, still ambiguous: ask the user. compact runs after this turn; fork copies it without switching.',
104
+ 'status defaults to current; list pages sessions, without sub-agent or archived ones unless include_agents/include_archived; bots shows each bot’s activity, work, approvals, routines and attention flags, or with session_id (a bot id or its chat) its routines and recent sessions; read returns latest items; send queues prompt (reportsBack: the turn result reports back here); cancel stops the turn; delete removes the session. create (bot conversations) starts a visible agent task session from brief (started, not done yet); unknown Project: list-projects first, still ambiguous: ask the user. compact runs after this turn; fork copies it without switching. suggest (Mixy’s chat only) records a proposal made to the user under key; value false records that the user declined it.',
105
+ },
106
+ title: {
107
+ type: 'string',
108
+ description: 'rename: new current-session title; create: task title; suggest: the proposal in one line.',
109
+ },
110
+ key: {
111
+ type: 'string',
112
+ description:
113
+ 'suggest: stable action:target key, e.g. pause-routine:daily-news; the same key refreshes its proposal.',
103
114
  },
104
- title: { type: 'string', description: 'rename: new current-session title; create: task title.' },
105
115
  session_id: {
106
116
  type: 'string',
107
117
  description:
@@ -150,7 +160,7 @@ export const SESSION_TOOL = {
150
160
  'Existing directory; implies set-project. Resolve names with list-projects first. Shell cd does not switch Project.',
151
161
  },
152
162
  compact: { type: 'boolean', description: 'fork: compact the copy.' },
153
- value: { type: 'boolean', description: 'favorite/archive: on or off.' },
163
+ value: { type: 'boolean', description: 'favorite/archive: on or off; suggest: false = the user declined it.' },
154
164
  brief: { type: 'string', description: 'create: self-contained instruction, the first message.' },
155
165
  project: { type: 'string', description: 'create: registered Project name or existing directory.' },
156
166
  worktree: {
@@ -171,7 +181,13 @@ export const SESSION_TOOL = {
171
181
  },
172
182
  model: {
173
183
  type: 'string',
174
- description: 'create: provider/model or preset; default is the Lead model (else the last-used one).',
184
+ description:
185
+ 'create: provider/model, preset, or a model menu display name (e.g. "Claude Opus 5.5", case-insensitive; a trailing effort word is read as effort); default is the Lead model (else the last-used one).',
186
+ },
187
+ effort: {
188
+ type: 'string',
189
+ description:
190
+ 'create/send: reasoning effort setting of the session (auto, or a level its model supports, e.g. low, medium, high, xhigh, max); wins over one in model. send: bots may change only task sessions they created. status shows effort and fast.',
175
191
  },
176
192
  notify: { type: 'boolean', description: 'create: report results back, default true.' },
177
193
  attachments: {
@@ -248,6 +264,7 @@ export function collectStandaloneToolDefs({
248
264
  visualizeToolDefs = [],
249
265
  setupToolDefs = [],
250
266
  askUserToolDefs = [],
267
+ reactionToolDefs = [],
251
268
  goalTools = [],
252
269
  agentTools = [],
253
270
  skillToolEnabled = true,
@@ -269,6 +286,7 @@ export function collectStandaloneToolDefs({
269
286
  ...admitted(visualizeToolDefs, VISUALIZE_RUNTIME_TOOLS),
270
287
  ...setupToolDefs,
271
288
  ...askUserToolDefs,
289
+ ...reactionToolDefs,
272
290
  ...goalTools,
273
291
  ...agentTools,
274
292
  ];
@@ -1,5 +1,11 @@
1
+ import { HEARTBEAT_SOURCE } from '../../../shared/routine-trigger.mjs';
2
+
1
3
  const INTERACTIVE_TOOL_PROFILE = 'interactive';
2
4
  export const HEADLESS_TOOL_PROFILE = 'headless';
5
+ // Mixy's hidden heartbeat session (session-runtime/mixy-heartbeat.mjs): it
6
+ // judges a reviewed digest, so it reads sessions and bots, records proposals
7
+ // (session) and pauses or reruns a routine (setup); nothing else.
8
+ export const HEARTBEAT_TOOL_PROFILE = 'heartbeat';
3
9
 
4
10
  export const HEADLESS_MODEL_TOOL_NAMES = Object.freeze([
5
11
  'find',
@@ -21,23 +27,42 @@ export const HEADLESS_MODEL_TOOL_NAMES = Object.freeze([
21
27
  'tidy',
22
28
  ]);
23
29
 
24
- const HEADLESS_MODEL_TOOL_SET = new Set(HEADLESS_MODEL_TOOL_NAMES.map((name) => name.toLowerCase()));
30
+ // load_tool loads the two deferred tools' schemas.
31
+ export const HEARTBEAT_MODEL_TOOL_NAMES = Object.freeze(['session', 'setup', 'load_tool']);
32
+
33
+ // The model tools each narrowed profile allows; the interactive profile has no list.
34
+ const PROFILE_TOOL_NAMES = new Map([
35
+ [HEADLESS_TOOL_PROFILE, HEADLESS_MODEL_TOOL_NAMES],
36
+ [HEARTBEAT_TOOL_PROFILE, HEARTBEAT_MODEL_TOOL_NAMES],
37
+ ]);
38
+ const PROFILE_TOOL_SETS = new Map(
39
+ [...PROFILE_TOOL_NAMES].map(([profile, names]) => [profile, new Set(names.map((name) => name.toLowerCase()))])
40
+ );
25
41
 
26
42
  export function normalizeToolProfile(value) {
27
- return String(value || '')
43
+ const profile = String(value || '')
28
44
  .trim()
29
- .toLowerCase() === HEADLESS_TOOL_PROFILE
30
- ? HEADLESS_TOOL_PROFILE
31
- : INTERACTIVE_TOOL_PROFILE;
45
+ .toLowerCase();
46
+ return PROFILE_TOOL_NAMES.has(profile) ? profile : INTERACTIVE_TOOL_PROFILE;
47
+ }
48
+
49
+ /** The profile a session runtime runs under: a session created as a heartbeat
50
+ * run (source `heartbeat`) is fenced to the heartbeat tools whatever profile
51
+ * was asked for. The source only ever narrows, so a forged one costs nothing. */
52
+ export function sessionToolProfile({ toolProfile, sessionProfile } = {}) {
53
+ if (String(sessionProfile?.sourceName || '').trim() === HEARTBEAT_SOURCE) return HEARTBEAT_TOOL_PROFILE;
54
+ return normalizeToolProfile(toolProfile);
32
55
  }
33
56
 
34
57
  export function modelToolSchemaAllowlist(profile) {
35
- return normalizeToolProfile(profile) === HEADLESS_TOOL_PROFILE ? [...HEADLESS_MODEL_TOOL_NAMES] : null;
58
+ const names = PROFILE_TOOL_NAMES.get(normalizeToolProfile(profile));
59
+ return names ? [...names] : null;
36
60
  }
37
61
 
38
62
  export function modelToolAllowedForProfile(name, profile) {
39
- if (normalizeToolProfile(profile) !== HEADLESS_TOOL_PROFILE) return true;
40
- return HEADLESS_MODEL_TOOL_SET.has(
63
+ const allowed = PROFILE_TOOL_SETS.get(normalizeToolProfile(profile));
64
+ if (!allowed) return true;
65
+ return allowed.has(
41
66
  String(name || '')
42
67
  .trim()
43
68
  .toLowerCase()
@@ -46,12 +71,12 @@ export function modelToolAllowedForProfile(name, profile) {
46
71
 
47
72
  export function filterModelToolsForProfile(tools, profile) {
48
73
  const rows = Array.isArray(tools) ? tools : [];
49
- if (normalizeToolProfile(profile) !== HEADLESS_TOOL_PROFILE) return rows;
74
+ if (!PROFILE_TOOL_SETS.has(normalizeToolProfile(profile))) return rows;
50
75
  return rows.filter((tool) => modelToolAllowedForProfile(tool?.name, profile));
51
76
  }
52
77
 
53
78
  export function disallowedModelToolNamesForProfile(tools, profile) {
54
- if (normalizeToolProfile(profile) !== HEADLESS_TOOL_PROFILE) return [];
79
+ if (!PROFILE_TOOL_SETS.has(normalizeToolProfile(profile))) return [];
55
80
  return [
56
81
  ...new Set(
57
82
  (Array.isArray(tools) ? tools : [])
@@ -3,9 +3,10 @@
3
3
  // shipped starter after the user deletes it, so package updates do not
4
4
  // silently resurrect the role.
5
5
  import { join } from 'node:path';
6
- import { existsSync } from 'node:fs';
6
+ import { existsSync, statSync } from 'node:fs';
7
7
  import { clean } from '../session-text.mjs';
8
- import { defaultDirResolver, readJsonSafe, readTextSafe } from '../fs-utils.mjs';
8
+ import { parseJsonText } from '../../../../shared/atomic-file.mjs';
9
+ import { defaultDirResolver, readTextSafe } from '../fs-utils.mjs';
9
10
  import { SHARED_SCAN_REVALIDATE_MS, createSharedScanCache } from './shared-scan-cache.mjs';
10
11
  import { isHiddenAgent } from '../../internal-agents.mjs';
11
12
  import { isAgentDisabled } from '../../../../shared/agent-route-config.mjs';
@@ -30,6 +31,27 @@ import {
30
31
  setAgentDefinitionCache,
31
32
  } from '../workflow-ids.mjs';
32
33
 
34
+ /** The entry file name an agent folder's manifest names (default AGENT.md). */
35
+ export const agentEntryFile = (manifest) => clean(manifest?.entry) || 'AGENT.md';
36
+
37
+ /** The entry file path of the agent folder `dir` for `manifest`, exactly as the
38
+ * loader reads it. The one resolver the loader, discovery, presence and the
39
+ * editor's revision share. */
40
+ export const agentEntryPath = (dir, manifest) => join(dir, agentEntryFile(manifest));
41
+
42
+ /** The agent.json object in `text` (a leading BOM tolerated), or null when it
43
+ * is not a JSON object. The one manifest parser: loader, discovery, editor. */
44
+ export function parseManifestText(text) {
45
+ try {
46
+ const value = parseJsonText(String(text));
47
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ const manifestOfText = (text) => parseManifestText(text) || {};
54
+
33
55
  // Yields its fs requests; see shared-scan-cache.mjs. With the bot store
34
56
  // active, bots/ is scanned too and the agents/ originals of the ids the split
35
57
  // copied are skipped (agent-store.mjs).
@@ -69,6 +91,21 @@ function* scanCustomAgentIds(userRoot, shippedRoot, botsRoot = null, migrated =
69
91
  export const AGENT_LIST_REVALIDATE_MS = SHARED_SCAN_REVALIDATE_MS;
70
92
  const agentListCache = createSharedScanCache();
71
93
 
94
+ // mtime+size of each file ("-" when absent). loadAgentDefinition runs per
95
+ // agent on every status pulse and prompt build; a handful of stats is cheap.
96
+ function fileSignature(files) {
97
+ return files
98
+ .map((file) => {
99
+ try {
100
+ const st = statSync(file);
101
+ return `${st.mtimeMs}:${st.size}`;
102
+ } catch {
103
+ return '-';
104
+ }
105
+ })
106
+ .join('|');
107
+ }
108
+
72
109
  export function createWorkflowAgents({ rootDir, dataDir, readMarkdownDocument, normalizeAgentPermissionOrNone }) {
73
110
  const defaultDir = defaultDirResolver(dataDir);
74
111
  function agentSourceDirs(dir, id) {
@@ -111,12 +148,24 @@ export function createWorkflowAgents({ rootDir, dataDir, readMarkdownDocument, n
111
148
  const agentId = normalizeAgentId(id) || normalizeWorkflowId(id);
112
149
  if (!agentId) return null;
113
150
  const cacheKey = `${dir || defaultDir()}\n${agentId}`;
114
- if (agentDefinitionCache.has(cacheKey)) return agentDefinitionCache.get(cacheKey);
151
+ const cached = agentDefinitionCache.get(cacheKey);
152
+ // A hit is trusted only while the files it was built from are unchanged,
153
+ // so a direct edit of AGENT.md/agent.json/PERSONA.md shows without a restart.
154
+ if (cached && fileSignature(cached.files) === cached.signature) return cached.definition;
115
155
  const slot = FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId);
156
+ const files = [];
157
+ const remember = (definition) => {
158
+ setAgentDefinitionCache(cacheKey, { definition, files, signature: fileSignature(files) });
159
+ return definition;
160
+ };
116
161
  for (const d of agentSourceDirs(dir, agentId)) {
117
- const manifest = readJsonSafe(join(d, 'agent.json')) || {};
118
- const entry = clean(manifest.entry) || 'AGENT.md';
119
- const doc = readMarkdownDocument(readTextSafe(join(d, entry)));
162
+ const manifest = manifestOfText(readTextSafe(join(d, 'agent.json')));
163
+ const entry = agentEntryPath(d, manifest);
164
+ // Shipped files change only with an install update (a restart).
165
+ if (d !== join(rootDir, 'agents', agentId)) {
166
+ files.push(join(d, 'agent.json'), entry, join(d, 'PERSONA.md'), join(d, AGENT_DELETED_MARKER));
167
+ }
168
+ const doc = readMarkdownDocument(readTextSafe(entry));
120
169
  const body = doc.body;
121
170
  if (!body) continue;
122
171
  const definition = {
@@ -136,13 +185,11 @@ export function createWorkflowAgents({ rootDir, dataDir, readMarkdownDocument, n
136
185
  // Optional PERSONA.md beside AGENT.md (same dir, same cache entry).
137
186
  persona: (readTextSafe(join(d, 'PERSONA.md')) || '').trim(),
138
187
  };
139
- setAgentDefinitionCache(cacheKey, definition);
140
- return definition;
188
+ return remember(definition);
141
189
  }
142
190
  // Every shipped and user role lives at agents/<id>/AGENT.md; there is no
143
191
  // flat agents/<id>.md layout left to fall back to.
144
- setAgentDefinitionCache(cacheKey, null);
145
- return null;
192
+ return remember(null);
146
193
  }
147
194
 
148
195
  return { listCustomAgentIds, delegatableAgentIds, loadAgentDefinition };
@@ -59,13 +59,13 @@ export function createWorkflowContext({ packs, agents }) {
59
59
  // - coding: the orchestration mode block and the full agent catalog;
60
60
  // - agent mode: the fixed delegation block (no per-agent setting) and the
61
61
  // sub-agent catalog — identical for every agent, so their prompts share
62
- // it — plus, for Mixy only, the contact-agent catalog.
63
- function leadPromptParts(config, dir, { agentMode = false, mixy = false } = {}) {
62
+ // it — plus the contact-agent catalog: every bot but `agentId` itself.
63
+ function leadPromptParts(config, dir, { agentMode = false, agentId = '' } = {}) {
64
64
  const mode = configuredOrchestrationMode(config);
65
65
  const ids = agents.delegatableAgentIds(config, dir);
66
66
  const isContact = (id) => isContactAgentId(id, agents.loadAgentDefinition(dir, id)?.kind);
67
67
  if (!agentMode) {
68
- // Bots are never a coding Lead's agents: only Mixy hands them work.
68
+ // Bots are never a coding Lead's agents: only agent-mode Leads hand them work.
69
69
  const workflowIds = ids.filter((id) => !isContact(id));
70
70
  const catalog = agentCatalog(dir, workflowIds);
71
71
  return {
@@ -79,7 +79,10 @@ export function createWorkflowContext({ packs, agents }) {
79
79
  dir,
80
80
  ids.filter((id) => !isContact(id))
81
81
  );
82
- const contacts = mixy ? agentCatalog(dir, ids.filter(isContact)) : '';
82
+ const contacts = agentCatalog(
83
+ dir,
84
+ ids.filter((id) => isContact(id) && id !== agentId)
85
+ );
83
86
  return {
84
87
  // An agent-mode Lead without agents has no one to delegate to at all.
85
88
  orchestrationContext: subAgents || contacts ? AGENT_MODE_DELEGATION : '',
@@ -110,8 +113,8 @@ export function createWorkflowContext({ packs, agents }) {
110
113
  // orchestration instructions / agent catalog.
111
114
  packContext,
112
115
  context: [packContext, orchestrationContextBlock(config, dir)].filter(Boolean).join('\n\n'),
113
- // A Lead's BP3 slices beside the pack: `{ agentMode, mixy }` picks the
114
- // coding, agent or Mixy variant (session/agent-mode.mjs).
116
+ // A Lead's BP3 slices beside the pack: `{ agentMode, agentId }` picks the
117
+ // coding variant, or the agent variant of the agent `agentId` (session/agent-mode.mjs).
115
118
  leadPromptParts: (options) => leadPromptParts(config, dir, options),
116
119
  };
117
120
  }
@@ -15,7 +15,7 @@ export const BUILTIN_SLOT_AGENT_IDS = new Set(
15
15
  FIXED_AGENT_SLOTS.filter((agent) => agent.workflowSlot).map((agent) => agent.id)
16
16
  );
17
17
  export const STARTER_AGENT_ORDER = new Map(DEFAULT_DISABLED_AGENT_IDS.map((id, index) => [id, index]));
18
- // Only Mixy delegates to contacts (agent-route-config.mjs isContactAgentId).
18
+ // Only agent-mode Leads (Mixy and the bots) delegate to contacts (agent-route-config.mjs isContactAgentId).
19
19
  export { isContactAgentId } from '../../../shared/agent-route-config.mjs';
20
20
  export const DEFAULT_WORKFLOW_ID = 'default';
21
21
 
@@ -10,8 +10,10 @@ import {
10
10
  import { repairTranscriptBeforeProviderSend } from './loop/transcript-repair.mjs';
11
11
  import { resetAccountProbePacing } from '../providers/account-pool.mjs';
12
12
  import { createLoopState, finishLoop } from './loop/loop-state.mjs';
13
+ import { clearReactionTargets } from '../runtime-core/reaction-tool-defs.mjs';
13
14
  import { applyRetryAction, beginIteration, sendProviderRequest, settleSendResult } from './loop/send-phase.mjs';
14
15
  import { runToolPhase } from './loop/tool-phase.mjs';
16
+ import { endedWithoutReply } from './loop/turn-limits.mjs';
15
17
 
16
18
  // Facade re-exports: these symbols live in ./loop/ modules and are part of
17
19
  // loop.mjs's public surface (imported by scripts/tests and other runtime
@@ -44,6 +46,16 @@ export {
44
46
  * ./loop/tool-phase.mjs.
45
47
  */
46
48
  export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
49
+ try {
50
+ return await runAgentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts);
51
+ } finally {
52
+ // A react call's recorded target belongs to this loop: whatever the batch
53
+ // did not consume (cancelled or failed turn) ends with it.
54
+ if (sendOpts?.sessionId) clearReactionTargets(sendOpts.sessionId);
55
+ }
56
+ }
57
+
58
+ async function runAgentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
47
59
  // An explicit request asks for a current answer. Let the account pool
48
60
  // re-measure quota it recorded as exhausted instead of refusing from the old
49
61
  // reading — once per loop entry, not per provider round inside the turn.
@@ -67,6 +79,12 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
67
79
  break;
68
80
  }
69
81
  await runToolPhase(state, round, sent, onToolCall);
82
+ // A capped turn that kept calling tools after it was told to reply ends
83
+ // here with no reply (./loop/turn-limits.mjs).
84
+ if (state.turnLimitEnded) {
85
+ state.response = endedWithoutReply(state.response);
86
+ break;
87
+ }
70
88
  }
71
89
  return finishLoop(state);
72
90
  }
@@ -84,15 +84,12 @@ export const ROOM_LIMITS: Readonly<
84
84
  Record<'maxRounds' | 'maxMessages', Readonly<{ min: number; max: number; default: number }>>
85
85
  >;
86
86
  export const ROOM_INSTRUCTION_LIMIT: number;
87
- export const ROOM_PASS_REPLY: string;
88
87
  export const ROOM_ROW_SOURCE: string;
89
- export function isRoomId(value: unknown): value is string;
90
88
  export function roomSessionId(roomId: unknown): string;
91
89
  export function roomIdOfSession(sessionId: unknown): string;
92
90
  export function roomMemberSessionId(roomId: unknown, agentId: unknown): string;
93
91
  export function roomMemberOfSession(sessionId: unknown): { roomId: string; agentId: string } | null;
94
92
  export function isRoomThreadSessionId(sessionId: unknown): boolean;
95
- export function clampRoomLimit(kind: 'maxRounds' | 'maxMessages', value: unknown): number;
96
93
  export function normalizeRoomConfig(value?: Partial<RoomConfig> & Record<string, unknown>): RoomConfig;
97
94
  export function parseRoomAsk(
98
95
  text: unknown,
@@ -11,7 +11,11 @@
11
11
  // session-runtime/handoff-attachments.mjs): stored references, handed as
12
12
  // content to the members the row reaches, once each.
13
13
  import { escapeHarnessTags } from '../../../shared/shell-output-summary.mjs';
14
- import { normalizeMessageAttachments, transcriptAttachmentFields } from '../../../shared/transcript-metadata.mjs';
14
+ import {
15
+ normalizeMessageAttachments,
16
+ transcriptAttachmentFields,
17
+ transcriptReactionsField,
18
+ } from '../../../shared/transcript-metadata.mjs';
15
19
  import { AGENT_MESSAGE_NOTE } from '../../../shared/agent-message-envelope.mjs';
16
20
  import { agentHomeSessionId } from './agent-home.mjs';
17
21
 
@@ -29,7 +33,7 @@ export const ROOM_LIMITS = Object.freeze({
29
33
  /** The longest supervisor note a member's turn carries. */
30
34
  export const ROOM_INSTRUCTION_LIMIT = 200;
31
35
  /** A member's reply that says it has nothing to add. */
32
- export const ROOM_PASS_REPLY = '(pass)';
36
+ const ROOM_PASS_REPLY = '(pass)';
33
37
  /** `meta.source` of every row stored in a room's log. */
34
38
  export const ROOM_ROW_SOURCE = 'agent-room';
35
39
  /** First-time members see at most this many earlier rows. */
@@ -44,7 +48,7 @@ const PART_TYPES = new Set(['image', 'file', 'text']);
44
48
  const ATTACHMENT_LIMIT = 64;
45
49
  const SENDER_SESSION = /^[A-Za-z0-9_.:-]{1,200}$/;
46
50
 
47
- export function isRoomId(value) {
51
+ function isRoomId(value) {
48
52
  return typeof value === 'string' && ROOM_ID.test(value);
49
53
  }
50
54
 
@@ -86,7 +90,7 @@ export function isRoomThreadSessionId(sessionId) {
86
90
  }
87
91
 
88
92
  /** `value` as a whole number inside the `kind` limit's bounds. */
89
- export function clampRoomLimit(kind, value) {
93
+ function clampRoomLimit(kind, value) {
90
94
  const bounds = ROOM_LIMITS[kind];
91
95
  const number = Math.round(Number(value));
92
96
  if (!Number.isFinite(number)) return bounds.default;
@@ -776,6 +780,8 @@ export function roomRowItem(message, id) {
776
780
  // A user row's (or member speech's) files, in the fields a chat prompt's row draws them from.
777
781
  ...(room.kind === 'user' || room.kind === 'speech' ? transcriptAttachmentFields(message.meta.transcript) : {}),
778
782
  ...(room.kind === 'user' && Array.isArray(images) && images.length ? { images } : {}),
783
+ // The members' emoji reactions on a user row (message-reactions.mjs).
784
+ ...(room.kind === 'user' ? transcriptReactionsField(message.meta.transcript) : {}),
779
785
  ...roomRowSender(room),
780
786
  };
781
787
  }
@@ -90,14 +90,24 @@ function splitFreshSource(messages) {
90
90
  };
91
91
  }
92
92
 
93
+ // A cut at `end` that would split a surrogate pair (an emoji) moves back one
94
+ // unit: a lone surrogate serializes as an escape providers reject as invalid
95
+ // JSON, failing every Compact of the session.
96
+ function codePointEnd(text, end) {
97
+ const code = text.charCodeAt(end - 1);
98
+ return end < text.length && code >= 0xd800 && code <= 0xdbff ? end - 1 : end;
99
+ }
100
+
93
101
  function chunkConversationMessage(role, content, source = {}) {
94
102
  const text = String(content || '');
95
103
  const out = [];
96
- for (let offset = 0; offset < text.length; offset += HANDOFF_SOURCE_CHUNK_CHARS) {
104
+ for (let offset = 0; offset < text.length; ) {
105
+ const end = codePointEnd(text, offset + HANDOFF_SOURCE_CHUNK_CHARS);
97
106
  const next = {
98
107
  role,
99
- content: text.slice(offset, offset + HANDOFF_SOURCE_CHUNK_CHARS),
108
+ content: text.slice(offset, end),
100
109
  };
110
+ offset = end;
101
111
  if (Object.hasOwn(source, 'ts')) next.ts = source.ts;
102
112
  if (Object.hasOwn(source, 'timestamp')) next.timestamp = source.timestamp;
103
113
  out.push(next);
@@ -113,7 +123,9 @@ const SUMMARY_TOOL_ARGS_CHARS = 200;
113
123
  const SUMMARY_TOOL_OUTCOME_CHARS = 300;
114
124
 
115
125
  function boundedSummaryText(text, limit) {
116
- return text.length > limit ? `${text.slice(0, limit)}… [${text.length - limit} more chars]` : text;
126
+ if (text.length <= limit) return text;
127
+ const end = codePointEnd(text, limit);
128
+ return `${text.slice(0, end)}… [${text.length - end} more chars]`;
117
129
  }
118
130
 
119
131
  function toolCallSummaryLine(call, result) {
@@ -11,6 +11,7 @@ import { createEagerEntry, runEagerEntry } from './eager-dispatch/entry.mjs';
11
11
  import { executeTool } from './loop/tool-exec.mjs';
12
12
  import { isParallelDispatchable } from './loop/tool-helpers.mjs';
13
13
  import { resolveExecutionToolCall } from './loop/tool-call-resolution.mjs';
14
+ import { turnLimitsOf } from './loop/turn-limits.mjs';
14
15
 
15
16
  export function createEagerDispatcher({
16
17
  tools,
@@ -37,6 +38,9 @@ export function createEagerDispatcher({
37
38
  repeatFailLimit,
38
39
  });
39
40
  const barriers = createEagerBarriers({ cwd });
41
+ // A capped turn runs its calls in order, so none past its cap starts early
42
+ // (loop/turn-limits.mjs).
43
+ const capped = turnLimitsOf(opts) !== null;
40
44
  const execute = (call, entry) =>
41
45
  executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, {
42
46
  toolCallId: call.id,
@@ -52,6 +56,7 @@ export function createEagerDispatcher({
52
56
  const startEagerTool = (wireCall) => {
53
57
  // Streamed call_tool requests are admitted, ordered and executed as their
54
58
  // targets; a projection error is answered by the serial batch instead.
59
+ if (capped) return null;
55
60
  const call = resolveExecutionToolCall(wireCall, sessionRef);
56
61
  if (!call?.id || pending.has(call.id) || !isParallelDispatchable(call.name)) return null;
57
62
  const admitted = admission.admit(call);