@dobby.ai/dobby 0.1.0 → 0.1.2

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 (156) hide show
  1. package/README.md +84 -39
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/commands/doctor.js +81 -2
  5. package/dist/src/cli/commands/extension.js +3 -1
  6. package/dist/src/cli/commands/init.js +43 -173
  7. package/dist/src/cli/commands/topology.js +38 -14
  8. package/dist/src/cli/program.js +15 -137
  9. package/dist/src/cli/shared/config-io.js +3 -31
  10. package/dist/src/cli/shared/config-mutators.js +33 -9
  11. package/dist/src/cli/shared/configure-sections.js +52 -12
  12. package/dist/src/cli/shared/init-catalog.js +89 -46
  13. package/dist/src/cli/shared/local-extension-specs.js +85 -0
  14. package/dist/src/cli/shared/schema-prompts.js +26 -2
  15. package/dist/src/core/gateway.js +3 -1
  16. package/dist/src/core/routing.js +53 -38
  17. package/dist/src/core/types.js +2 -0
  18. package/dist/src/cron/config.js +2 -2
  19. package/dist/src/cron/service.js +87 -23
  20. package/dist/src/cron/store.js +1 -1
  21. package/dist/src/main.js +0 -0
  22. package/dist/src/shared/dobby-repo.js +40 -0
  23. package/package.json +11 -4
  24. package/.env.example +0 -9
  25. package/AGENTS.md +0 -267
  26. package/ROADMAP.md +0 -34
  27. package/config/cron.example.json +0 -9
  28. package/config/gateway.example.json +0 -128
  29. package/config/models.custom.example.json +0 -27
  30. package/dist/src/agent/tests/event-forwarder.test.js +0 -113
  31. package/dist/src/cli/shared/config-path.js +0 -207
  32. package/dist/src/cli/shared/init-models-file.js +0 -65
  33. package/dist/src/cli/shared/presets.js +0 -86
  34. package/dist/src/cli/tests/config-command.test.js +0 -42
  35. package/dist/src/cli/tests/config-io.test.js +0 -64
  36. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  37. package/dist/src/cli/tests/config-path.test.js +0 -21
  38. package/dist/src/cli/tests/discord-config.test.js +0 -23
  39. package/dist/src/cli/tests/doctor.test.js +0 -107
  40. package/dist/src/cli/tests/init-catalog.test.js +0 -87
  41. package/dist/src/cli/tests/presets.test.js +0 -41
  42. package/dist/src/cli/tests/program-options.test.js +0 -92
  43. package/dist/src/cli/tests/routing-config.test.js +0 -199
  44. package/dist/src/cli/tests/routing-legacy.test.js +0 -191
  45. package/dist/src/core/tests/control-command.test.js +0 -17
  46. package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
  47. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  48. package/dist/src/core/tests/typing-controller.test.js +0 -103
  49. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  50. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  51. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  52. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  53. package/docs/MVP.md +0 -135
  54. package/docs/RUNBOOK.md +0 -242
  55. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  56. package/plugins/connector-discord/dobby.manifest.json +0 -18
  57. package/plugins/connector-discord/index.js +0 -1
  58. package/plugins/connector-discord/package-lock.json +0 -360
  59. package/plugins/connector-discord/package.json +0 -38
  60. package/plugins/connector-discord/src/connector.ts +0 -350
  61. package/plugins/connector-discord/src/contribution.ts +0 -21
  62. package/plugins/connector-discord/src/mapper.ts +0 -102
  63. package/plugins/connector-discord/tsconfig.json +0 -19
  64. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  65. package/plugins/connector-feishu/index.js +0 -1
  66. package/plugins/connector-feishu/package-lock.json +0 -618
  67. package/plugins/connector-feishu/package.json +0 -38
  68. package/plugins/connector-feishu/src/connector.ts +0 -343
  69. package/plugins/connector-feishu/src/contribution.ts +0 -26
  70. package/plugins/connector-feishu/src/mapper.ts +0 -401
  71. package/plugins/connector-feishu/tsconfig.json +0 -19
  72. package/plugins/plugin-sdk/index.d.ts +0 -261
  73. package/plugins/plugin-sdk/index.js +0 -1
  74. package/plugins/plugin-sdk/package-lock.json +0 -12
  75. package/plugins/plugin-sdk/package.json +0 -22
  76. package/plugins/provider-claude/dobby.manifest.json +0 -17
  77. package/plugins/provider-claude/index.js +0 -1
  78. package/plugins/provider-claude/package-lock.json +0 -3398
  79. package/plugins/provider-claude/package.json +0 -39
  80. package/plugins/provider-claude/src/contribution.ts +0 -1018
  81. package/plugins/provider-claude/tsconfig.json +0 -19
  82. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  83. package/plugins/provider-claude-cli/index.js +0 -1
  84. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  85. package/plugins/provider-claude-cli/package.json +0 -38
  86. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  87. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  88. package/plugins/provider-pi/dobby.manifest.json +0 -17
  89. package/plugins/provider-pi/index.js +0 -1
  90. package/plugins/provider-pi/package-lock.json +0 -3877
  91. package/plugins/provider-pi/package.json +0 -40
  92. package/plugins/provider-pi/src/contribution.ts +0 -476
  93. package/plugins/provider-pi/tsconfig.json +0 -19
  94. package/plugins/sandbox-core/boxlite.js +0 -1
  95. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  96. package/plugins/sandbox-core/docker.js +0 -1
  97. package/plugins/sandbox-core/package-lock.json +0 -136
  98. package/plugins/sandbox-core/package.json +0 -39
  99. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  100. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  101. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  102. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  103. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  104. package/plugins/sandbox-core/tsconfig.json +0 -19
  105. package/scripts/local-extensions.mjs +0 -168
  106. package/src/agent/event-forwarder.ts +0 -414
  107. package/src/cli/commands/config.ts +0 -328
  108. package/src/cli/commands/configure.ts +0 -92
  109. package/src/cli/commands/cron.ts +0 -410
  110. package/src/cli/commands/doctor.ts +0 -230
  111. package/src/cli/commands/extension.ts +0 -205
  112. package/src/cli/commands/init.ts +0 -396
  113. package/src/cli/commands/start.ts +0 -223
  114. package/src/cli/commands/topology.ts +0 -383
  115. package/src/cli/index.ts +0 -9
  116. package/src/cli/program.ts +0 -465
  117. package/src/cli/shared/config-io.ts +0 -277
  118. package/src/cli/shared/config-mutators.ts +0 -440
  119. package/src/cli/shared/config-schema.ts +0 -228
  120. package/src/cli/shared/config-types.ts +0 -121
  121. package/src/cli/shared/configure-sections.ts +0 -551
  122. package/src/cli/shared/discord-config.ts +0 -14
  123. package/src/cli/shared/init-catalog.ts +0 -189
  124. package/src/cli/shared/init-models-file.ts +0 -77
  125. package/src/cli/shared/runtime.ts +0 -33
  126. package/src/cli/shared/schema-prompts.ts +0 -414
  127. package/src/cli/tests/config-command.test.ts +0 -56
  128. package/src/cli/tests/config-io.test.ts +0 -92
  129. package/src/cli/tests/config-mutators.test.ts +0 -59
  130. package/src/cli/tests/doctor.test.ts +0 -120
  131. package/src/cli/tests/init-catalog.test.ts +0 -96
  132. package/src/cli/tests/program-options.test.ts +0 -113
  133. package/src/cli/tests/routing-config.test.ts +0 -209
  134. package/src/core/control-command.ts +0 -12
  135. package/src/core/dedup-store.ts +0 -103
  136. package/src/core/gateway.ts +0 -607
  137. package/src/core/routing.ts +0 -379
  138. package/src/core/runtime-registry.ts +0 -141
  139. package/src/core/tests/control-command.test.ts +0 -20
  140. package/src/core/tests/runtime-registry.test.ts +0 -140
  141. package/src/core/tests/typing-controller.test.ts +0 -129
  142. package/src/core/types.ts +0 -318
  143. package/src/core/typing-controller.ts +0 -119
  144. package/src/cron/config.ts +0 -154
  145. package/src/cron/schedule.ts +0 -61
  146. package/src/cron/service.ts +0 -249
  147. package/src/cron/store.ts +0 -155
  148. package/src/cron/types.ts +0 -60
  149. package/src/extension/loader.ts +0 -145
  150. package/src/extension/manager.ts +0 -355
  151. package/src/extension/manifest.ts +0 -26
  152. package/src/extension/registry.ts +0 -229
  153. package/src/main.ts +0 -8
  154. package/src/sandbox/executor.ts +0 -44
  155. package/src/sandbox/host-executor.ts +0 -118
  156. package/tsconfig.json +0 -18
package/README.md CHANGED
@@ -7,6 +7,7 @@ Discord-first 本地 Agent Gateway。宿主只负责 CLI、网关主流程、扩
7
7
  - `@dobby.ai/connector-discord`
8
8
  - `@dobby.ai/connector-feishu`
9
9
  - `@dobby.ai/provider-pi`
10
+ - `@dobby.ai/provider-codex-cli`
10
11
  - `@dobby.ai/provider-claude-cli`
11
12
  - `@dobby.ai/provider-claude`
12
13
  - `@dobby.ai/sandbox-core`
@@ -17,14 +18,14 @@ Discord-first 本地 Agent Gateway。宿主只负责 CLI、网关主流程、扩
17
18
 
18
19
  - connector source -> binding -> route -> provider / sandbox
19
20
  - Discord 频道 / 线程接入;线程消息继续按父频道命中 binding
20
- - Feishu 长连接消息接入(self-built app,手工安装/配置)
21
+ - Feishu 长连接消息接入(self-built app
21
22
  - Feishu 出站支持普通文本和 Markdown 卡片;默认群内直发,不走 reply thread
22
23
  - conversation 级 runtime 复用与串行化
23
24
  - 扩展 store 安装、启用、列举与 schema 驱动配置
24
25
  - Discord 流式回复、typing、附件下载与图片输入
25
26
  - cron 调度:一次性、固定间隔、cron expression
26
- - 交互式初始化:`dobby init`
27
- - 交互式配置:`dobby configure` / `dobby config edit`
27
+ - 交互式初始化:`dobby init`(支持多 provider / 多 connector starter)
28
+ - 配置检查与 schema inspect:`dobby config show|list|schema`
28
29
  - 诊断与保守修复:`dobby doctor [--fix]`
29
30
 
30
31
  ## 架构概览
@@ -59,7 +60,7 @@ Discord / Cron
59
60
  - npm
60
61
  - 对应 provider / connector 的外部运行条件
61
62
  - 例如 Discord bot token
62
- - Claude CLI 或 Claude Agent SDK 所需认证
63
+ - Codex CLI、Claude CLI 或 Claude Agent SDK 所需认证
63
64
  - 可选的 Docker / Boxlite 运行环境
64
65
 
65
66
  ## 快速开始
@@ -76,7 +77,7 @@ npm install
76
77
  npm run build
77
78
  ```
78
79
 
79
- 3. 初始化最小可运行配置
80
+ 3. 初始化模板配置
80
81
 
81
82
  ```bash
82
83
  npm run start -- init
@@ -84,21 +85,41 @@ npm run start -- init
84
85
 
85
86
  `init` 会做这些事情:
86
87
 
87
- - 交互选择 provider 和 connector
88
+ - 交互选择 provider 和 connector(均可多选)
88
89
  - 自动安装所选扩展到运行时 extension store
89
- - 优先使用扩展暴露的 `configSchema` 生成配置
90
+ - 写入一份带占位符的 `gateway.json` 模板
91
+ - 把 `routes.default.projectRoot` 设为当前工作目录
92
+ - 为 direct message 生成 `bindings.default`,回落到默认 route
93
+ - 为每个所选 connector 生成一个默认 binding 到同一条 route
90
94
  - 生成 `gateway.json`
91
- - 选择 `provider.pi` 且缺少 `models.custom.json` 时,自动创建该文件
95
+ - `provider.pi` 默认写入最小 inline 配置,不再依赖 `models.custom.json`
92
96
 
93
- 说明:当前 `init` 仍只内建 Discord connector。Feishu connector 需要通过 `extension install` + `config edit/configure` 手工启用。
97
+ 说明:当前 `init` 内建这些 starter 选择:
94
98
 
95
- 4. 运行诊断
99
+ - provider:`provider.pi`、`provider.claude-cli`
100
+ - connector:`connector.discord`、`connector.feishu`
101
+
102
+ 4. 编辑 `gateway.json`
103
+
104
+ 把 `REPLACE_WITH_*` / `YOUR_*` 占位值替换成你的真实配置,例如:
105
+
106
+ - `connectors.items[*]` 中的 token / appId / appSecret
107
+ - `bindings.items[*].source.id`
108
+ - `routes.items[*].projectRoot`(如需覆盖默认 project root)
109
+
110
+ 5. 运行诊断
96
111
 
97
112
  ```bash
98
113
  npm run start -- doctor
99
114
  ```
100
115
 
101
- 5. 启动网关
116
+ `doctor` 会同时检查:
117
+
118
+ - 配置结构 / 引用关系
119
+ - 缺失的扩展安装
120
+ - `REPLACE_WITH_*` / `YOUR_*` 这类 init 占位值是否还未替换
121
+
122
+ 6. 启动网关
102
123
 
103
124
  ```bash
104
125
  npm run start --
@@ -107,7 +128,9 @@ npm run start --
107
128
  说明:
108
129
 
109
130
  - `dobby` 无子命令时,默认等价于 `dobby start`
131
+ - `dobby --version` 可直接查看当前 CLI 版本
110
132
  - 在仓库内直接运行时,CLI 会自动使用 `./config/gateway.json`
133
+ - 在仓库内执行 `init` / `extension install` 时,会优先安装 `plugins/*` 的本地构建产物
111
134
  - 也可以通过环境变量覆盖配置路径:
112
135
 
113
136
  ```bash
@@ -152,33 +175,23 @@ cron 配置路径优先级:
152
175
  顶层命令:
153
176
 
154
177
  ```bash
178
+ dobby --version
155
179
  dobby start
156
180
  dobby init
157
- dobby configure
158
181
  dobby doctor [--fix]
159
182
  ```
160
183
 
161
- 配置与拓扑:
184
+ 配置检查:
162
185
 
163
186
  ```bash
164
187
  dobby config show [section] [--json]
165
188
  dobby config list [section] [--json]
166
- dobby config edit [--section provider|connector|route|binding]
167
189
  dobby config schema list [--json]
168
190
  dobby config schema show <contributionId> [--json]
169
-
170
- dobby bot list [--json]
171
- dobby bot set <connectorId> [--name <name>] [--token <token>]
172
-
173
- dobby binding list [--connector <id>] [--json]
174
- dobby binding set <bindingId> --connector <id> --source-type channel|chat --source-id <id> --route <id>
175
- dobby binding remove <bindingId>
176
-
177
- dobby route list [--json]
178
- dobby route set <routeId> [--project-root <path>] [--tools full|readonly] [--provider <id>] [--sandbox <id>] [--mentions required|optional]
179
- dobby route remove <routeId> [--cascade-bindings]
180
191
  ```
181
192
 
193
+ 配置变更建议直接编辑 `gateway.json`,再通过 `dobby doctor` 或 `dobby start` 做校验。
194
+
182
195
  扩展管理:
183
196
 
184
197
  ```bash
@@ -221,25 +234,40 @@ dobby cron remove <jobId>
221
234
  - 默认 provider instance ID
222
235
  - `providers.items[*].type` / `connectors.items[*].type` / `sandboxes.items[*].type`
223
236
  - 指向某个 contribution,实例配置直接内联在对象里
224
- - `routes.defaults`
225
- - 统一提供 route 默认的 `provider`、`sandbox`、`tools`、`mentions`
237
+ - `routes.default`
238
+ - 统一提供 route 默认的 `projectRoot`、`provider`、`sandbox`、`tools`、`mentions`
226
239
  - `routes.items[*]`
227
- - route 是可复用的执行 profile,定义 `projectRoot`,并按需覆盖 `provider`、`sandbox`、`tools`、`mentions`、`systemPromptFile`
240
+ - route 是可复用的执行 profile,可继承默认 `projectRoot`,并按需覆盖 `provider`、`sandbox`、`tools`、`mentions`、`systemPromptFile`
241
+ - `bindings.default`
242
+ - direct message 未命中显式 binding 时使用的默认 route fallback
228
243
  - `bindings.items[*]`
229
244
  - `(connector, source.type, source.id) -> route` 的入口绑定
230
245
  - `sandboxes.default`
231
246
  - 未指定时默认使用 `host.builtin`
232
- - 未匹配 binding 的入站消息会被直接忽略,不存在 default route fallback
233
-
234
- 当前代码还保留但未真正生效的字段:
235
-
236
- - cron job 的 `sessionPolicy`
247
+ - 未匹配 binding 的入站消息会被直接忽略;仅 direct message 可回落到 `bindings.default`
237
248
 
238
249
  示例配置:
239
250
 
240
251
  - gateway:[`config/gateway.example.json`](config/gateway.example.json)
241
252
  - cron:[`config/cron.example.json`](config/cron.example.json)
242
- - provider.pi 自定义模型:[`config/models.custom.example.json`](config/models.custom.example.json)
253
+
254
+ `provider.pi` 现在使用 inline custom provider 配置。最小常用字段是:
255
+
256
+ - `model`
257
+ - `baseUrl`
258
+ - `apiKey`
259
+
260
+ 这些字段默认自动补齐:
261
+
262
+ - `provider = "custom-openai"`
263
+ - `api = "openai-completions"`
264
+ - `authHeader = false`
265
+ - `thinkingLevel = "off"`
266
+ - `models = [{ id: model }]`
267
+
268
+ 只有在你需要多模型元数据或覆盖能力参数时,才需要手工展开 `models`。
269
+
270
+ `apiKey` 支持直接写 literal,也支持写环境变量名,由 `pi` 的 `AuthStorage` / `ModelRegistry` 按上游规则解析。
243
271
 
244
272
  ## 扩展包与 contribution
245
273
 
@@ -247,6 +275,7 @@ dobby cron remove <jobId>
247
275
 
248
276
  - `connector.discord`
249
277
  - `provider.pi`
278
+ - `provider.codex-cli`
250
279
  - `provider.claude-cli`
251
280
  - `provider.claude`
252
281
  - `sandbox.boxlite`
@@ -255,15 +284,30 @@ dobby cron remove <jobId>
255
284
  `dobby init` 当前只内建这些 starter 选择:
256
285
 
257
286
  - provider:`provider.pi`、`provider.claude-cli`
258
- - connector:`connector.discord`
287
+ - connector:`connector.discord`、`connector.feishu`
259
288
 
260
- `provider.claude` 与 sandbox 相关扩展需要手工安装和配置,例如:
289
+ `provider.codex-cli`、`provider.claude` 与 sandbox 相关扩展需要手工安装和配置,例如:
261
290
 
262
291
  ```bash
292
+ npm run start -- extension install @dobby.ai/provider-codex-cli --enable
263
293
  npm run start -- extension install @dobby.ai/provider-claude --enable
264
294
  npm run start -- extension install @dobby.ai/sandbox-core --enable
265
295
  ```
266
296
 
297
+ 若使用 `provider.codex-cli`,启动前建议检查:
298
+
299
+ ```bash
300
+ codex --version
301
+ codex login status
302
+ ```
303
+
304
+ 最小配置字段:
305
+
306
+ - `command`(默认 `codex`)
307
+ - `commandArgs`(默认 `[]`)
308
+ - `model`(可选;未设置时使用 Codex CLI / 本地配置默认模型)
309
+ - `skipGitRepoCheck`(默认 `false`)
310
+
267
311
  `--enable` 的行为:
268
312
 
269
313
  - 把包写入 `extensions.allowList`
@@ -292,13 +336,14 @@ npm run start -- cron add daily-report \
292
336
 
293
337
  说明:
294
338
 
295
- - `cron run <jobId>` 只是把 job 标记为“下一次 scheduler tick 执行”
339
+ - `cron run <jobId>` 会额外排队一次立即执行,不会恢复 paused 状态,也不会改写原有 `nextRunAtMs`
296
340
  - 需要已有一个正在运行的 `dobby start`
297
341
  - 当前 scheduled run 一律按 stateless / ephemeral 执行
298
342
 
299
343
  ## Discord 连接器的当前行为
300
344
 
301
- - 只处理已绑定的 guild channel,DM 目前禁用
345
+ - guild channel 仍按显式 binding 匹配
346
+ - DM 可通过 `bindings.default` 回落到默认 route
302
347
  - 线程消息使用父频道 ID 做 binding 查找
303
348
  - 会自动下载附件到本地
304
349
  - 图片会作为 image input 传给 provider
@@ -372,7 +417,7 @@ npm run plugins:build
372
417
 
373
418
  - `npm run dev:local` 与 `npm run start:local` 会尝试读取 `.env`
374
419
  - 普通 `npm run start -- ...` 不会自动载入 `.env`
375
- - 配置编辑流优先使用扩展 `configSchema`,无 schema 时退回 JSON 输入
420
+ - `dobby init` 生成的是模板配置;运行前先替换 `gateway.json` 中的 placeholder
376
421
 
377
422
  ## 相关文档
378
423
 
@@ -1,3 +1,11 @@
1
+ import { OUTBOUND_MESSAGE_KIND_METADATA_KEY, OUTBOUND_MESSAGE_KIND_PROGRESS, } from "../core/types.js";
2
+ const DEFAULT_PROGRESS_DEBOUNCE_MS = 150;
3
+ const DEFAULT_LONG_PROGRESS_MS = 10_000;
4
+ const WORKING_LOCALLY_TEXT = "Working locally...";
5
+ const STILL_WORKING_LOCALLY_TEXT = "Still working locally...";
6
+ const WORKING_WITH_TOOLS_TEXT = "Working with tools...";
7
+ const STILL_WORKING_WITH_TOOLS_TEXT = "Still working with tools...";
8
+ const RECOVERING_FROM_TOOL_ISSUE_TEXT = "Recovering from a tool issue...";
1
9
  function truncate(text, max) {
2
10
  if (max === undefined)
3
11
  return text;
@@ -57,22 +65,37 @@ export class EventForwarder {
57
65
  appendEmittedText = "";
58
66
  pendingFlush = null;
59
67
  flushSerial = Promise.resolve();
68
+ progressSerial = Promise.resolve();
60
69
  pendingOps = [];
61
70
  updateIntervalMs;
71
+ progressDebounceMs;
72
+ longProgressMs;
62
73
  toolMessageMode;
63
74
  maxTextLength;
64
75
  onOutboundActivity;
65
76
  updateStrategy;
77
+ progressUpdateStrategy;
66
78
  lastEditPrimaryText = null;
79
+ progressMessageId = null;
80
+ lastProgressMessageText = null;
81
+ pendingProgressText = null;
82
+ pendingProgressFlush = null;
83
+ hasQueuedProgressMessage = false;
84
+ longProgressTimer = null;
85
+ activeWorkPhase = null;
67
86
  constructor(connector, inbound, rootMessageId, logger, options = {}) {
68
87
  this.connector = connector;
69
88
  this.inbound = inbound;
70
89
  this.logger = logger;
71
90
  this.rootMessageId = rootMessageId;
72
91
  this.updateIntervalMs = options.updateIntervalMs ?? 400;
92
+ this.progressDebounceMs = options.progressDebounceMs ?? DEFAULT_PROGRESS_DEBOUNCE_MS;
93
+ this.longProgressMs = options.longProgressMs ?? DEFAULT_LONG_PROGRESS_MS;
73
94
  this.toolMessageMode = options.toolMessageMode ?? "none";
74
95
  this.onOutboundActivity = options.onOutboundActivity;
75
96
  this.updateStrategy = this.connector.capabilities.updateStrategy;
97
+ this.progressUpdateStrategy = this.connector.capabilities.progressUpdateStrategy
98
+ ?? (this.updateStrategy === "edit" ? "edit" : "create");
76
99
  const capabilityMaxTextLength = this.connector.capabilities.maxTextLength;
77
100
  this.maxTextLength = typeof capabilityMaxTextLength === "number" && capabilityMaxTextLength > 0
78
101
  ? capabilityMaxTextLength
@@ -98,13 +121,20 @@ export class EventForwarder {
98
121
  }
99
122
  return;
100
123
  }
124
+ if (event.type === "command_start") {
125
+ this.enterWorkPhase("local");
126
+ return;
127
+ }
101
128
  if (event.type === "tool_start") {
102
129
  this.logger.info({
103
130
  toolName: event.toolName,
104
131
  conversation: `${this.inbound.platform}:${this.inbound.accountId}:${this.inbound.chatId}:${this.inbound.threadId ?? "root"}`,
105
132
  }, "Tool execution started");
106
- if (this.updateStrategy !== "final_only" && this.toolMessageMode === "all") {
107
- this.enqueueSend(`_-> Running tool: ${event.toolName}_`);
133
+ if (this.toolMessageMode === "all") {
134
+ this.enqueueSideMessage(this.renderToolStartMessage(event.toolName));
135
+ }
136
+ else {
137
+ this.enterWorkPhase("tool");
108
138
  }
109
139
  return;
110
140
  }
@@ -115,26 +145,21 @@ export class EventForwarder {
115
145
  isError: event.isError,
116
146
  conversation: `${this.inbound.platform}:${this.inbound.accountId}:${this.inbound.chatId}:${this.inbound.threadId ?? "root"}`,
117
147
  }, event.isError ? "Tool execution finished with error" : "Tool execution finished");
118
- if (this.updateStrategy !== "final_only" &&
119
- (this.toolMessageMode === "all" || (this.toolMessageMode === "errors" && event.isError))) {
120
- const prefix = event.isError ? "ERR" : "OK";
121
- const header = `*${prefix} ${event.toolName}*\n\`\`\`\n`;
122
- const footer = "\n```";
123
- const availableSummaryLength = this.maxTextLength === undefined
124
- ? undefined
125
- : Math.max(0, this.maxTextLength - header.length - footer.length);
126
- this.enqueueSend(`${header}${truncate(summary, availableSummaryLength)}${footer}`);
148
+ if ((this.toolMessageMode === "all" || (this.toolMessageMode === "errors" && event.isError))) {
149
+ this.enqueueSideMessage(this.renderToolEndMessage(event.toolName, event.isError, summary));
150
+ }
151
+ else if (event.isError) {
152
+ this.setProgressMessage(RECOVERING_FROM_TOOL_ISSUE_TEXT);
127
153
  }
128
154
  return;
129
155
  }
130
156
  if (event.type === "status") {
131
- if (this.updateStrategy === "final_only") {
132
- return;
133
- }
134
- this.enqueueSend(`_${event.message}_`);
157
+ this.setProgressMessage(event.message);
135
158
  }
136
159
  };
137
160
  async finalize() {
161
+ this.clearLongProgressTimer();
162
+ await this.flushProgressUpdates();
138
163
  if (this.updateStrategy === "final_only") {
139
164
  await this.finalizeFinalOnly();
140
165
  await Promise.allSettled(this.pendingOps);
@@ -173,6 +198,7 @@ export class EventForwarder {
173
198
  return;
174
199
  }
175
200
  try {
201
+ await this.flushProgressUpdates();
176
202
  if (this.updateStrategy === "append") {
177
203
  await this.flushAppendProgress();
178
204
  return;
@@ -198,13 +224,113 @@ export class EventForwarder {
198
224
  });
199
225
  await run;
200
226
  }
201
- enqueueSend(text) {
227
+ enqueueSideMessage(text) {
228
+ this.enqueueSendWithMetadata(text, this.progressMessageMetadata());
229
+ }
230
+ setProgressMessage(message) {
231
+ this.activeWorkPhase = null;
232
+ this.clearLongProgressTimer();
233
+ this.scheduleProgressUpdate(this.renderStatusMessage(message));
234
+ }
235
+ enterWorkPhase(phase) {
236
+ if (this.activeWorkPhase === "local" && phase === "tool") {
237
+ return;
238
+ }
239
+ if (this.activeWorkPhase === phase) {
240
+ return;
241
+ }
242
+ this.activeWorkPhase = phase;
243
+ this.scheduleProgressUpdate(this.renderWorkPhaseMessage(phase, false));
244
+ this.scheduleLongProgressUpdate(phase);
245
+ }
246
+ scheduleProgressUpdate(text) {
247
+ const shouldCreateImmediately = !this.hasQueuedProgressMessage;
248
+ this.hasQueuedProgressMessage = true;
249
+ this.pendingProgressText = text;
250
+ if (shouldCreateImmediately) {
251
+ void this.flushProgressNow();
252
+ return;
253
+ }
254
+ if (this.pendingProgressFlush) {
255
+ clearTimeout(this.pendingProgressFlush);
256
+ }
257
+ this.pendingProgressFlush = setTimeout(() => {
258
+ void this.flushProgressNow();
259
+ }, this.progressDebounceMs);
260
+ }
261
+ async flushProgressNow() {
262
+ if (this.pendingProgressFlush) {
263
+ clearTimeout(this.pendingProgressFlush);
264
+ this.pendingProgressFlush = null;
265
+ }
266
+ const text = this.pendingProgressText;
267
+ if (text === null) {
268
+ return;
269
+ }
270
+ this.pendingProgressText = null;
271
+ const metadata = this.progressMessageMetadata();
272
+ const run = this.progressSerial.then(async () => {
273
+ if (this.lastProgressMessageText === text) {
274
+ return;
275
+ }
276
+ try {
277
+ if (this.canEditProgressMessage() && this.progressMessageId) {
278
+ await this.connector.send({
279
+ ...this.baseEnvelope(),
280
+ mode: "update",
281
+ targetMessageId: this.progressMessageId,
282
+ text,
283
+ metadata,
284
+ });
285
+ this.lastProgressMessageText = text;
286
+ this.noteOutboundActivity();
287
+ return;
288
+ }
289
+ const created = await this.connector.send({
290
+ ...this.baseEnvelope(),
291
+ mode: "create",
292
+ text,
293
+ metadata,
294
+ });
295
+ this.progressMessageId = this.canEditProgressMessage() ? (created.messageId ?? this.progressMessageId) : null;
296
+ this.lastProgressMessageText = text;
297
+ this.noteOutboundActivity();
298
+ }
299
+ catch (error) {
300
+ this.logger.warn({
301
+ err: error,
302
+ connectorId: this.inbound.connectorId,
303
+ chatId: this.inbound.chatId,
304
+ progressMessageId: this.progressMessageId,
305
+ }, "Failed to send or update progress message");
306
+ }
307
+ });
308
+ this.progressSerial = run.catch(() => {
309
+ // keep chain alive for future progress updates
310
+ });
311
+ this.pendingOps.push(run);
312
+ await run;
313
+ }
314
+ scheduleLongProgressUpdate(phase) {
315
+ this.clearLongProgressTimer();
316
+ if (this.longProgressMs <= 0) {
317
+ return;
318
+ }
319
+ this.longProgressTimer = setTimeout(() => {
320
+ if (this.activeWorkPhase !== phase) {
321
+ return;
322
+ }
323
+ this.scheduleProgressUpdate(this.renderWorkPhaseMessage(phase, true));
324
+ }, this.longProgressMs);
325
+ }
326
+ enqueueSendWithMetadata(text, metadata) {
202
327
  const promise = this.connector
203
328
  .send({
204
329
  ...this.baseEnvelope(),
205
330
  mode: "create",
206
331
  ...(this.rootMessageId ? { replyToMessageId: this.rootMessageId } : {}),
207
332
  text,
333
+ ...(metadata ? { metadata } : {}),
208
334
  })
209
335
  .then(() => {
210
336
  this.noteOutboundActivity();
@@ -219,6 +345,49 @@ export class EventForwarder {
219
345
  });
220
346
  this.pendingOps.push(promise);
221
347
  }
348
+ canEditProgressMessage() {
349
+ return this.progressUpdateStrategy === "edit";
350
+ }
351
+ progressMessageMetadata() {
352
+ return {
353
+ [OUTBOUND_MESSAGE_KIND_METADATA_KEY]: OUTBOUND_MESSAGE_KIND_PROGRESS,
354
+ };
355
+ }
356
+ renderStatusMessage(message) {
357
+ return truncate(message, this.maxTextLength);
358
+ }
359
+ renderWorkPhaseMessage(phase, isLongRunning) {
360
+ if (phase === "local") {
361
+ return truncate(isLongRunning ? STILL_WORKING_LOCALLY_TEXT : WORKING_LOCALLY_TEXT, this.maxTextLength);
362
+ }
363
+ return truncate(isLongRunning ? STILL_WORKING_WITH_TOOLS_TEXT : WORKING_WITH_TOOLS_TEXT, this.maxTextLength);
364
+ }
365
+ renderToolStartMessage(toolName) {
366
+ return truncate(`Running tool: ${toolName}`, this.maxTextLength);
367
+ }
368
+ renderToolEndMessage(toolName, isError, summary) {
369
+ const prefix = isError ? "ERR" : "OK";
370
+ const body = summary.trim().length > 0 ? `${prefix} ${toolName}\n${summary}` : `${prefix} ${toolName}`;
371
+ return truncate(body, this.maxTextLength);
372
+ }
373
+ async flushProgressUpdates() {
374
+ if (this.pendingProgressText !== null) {
375
+ await this.flushProgressNow();
376
+ return;
377
+ }
378
+ try {
379
+ await this.progressSerial;
380
+ }
381
+ catch {
382
+ // progress message failures are already logged when they happen
383
+ }
384
+ }
385
+ clearLongProgressTimer() {
386
+ if (this.longProgressTimer) {
387
+ clearTimeout(this.longProgressTimer);
388
+ this.longProgressTimer = null;
389
+ }
390
+ }
222
391
  async sendEditPrimary(text) {
223
392
  if (this.rootMessageId && this.lastEditPrimaryText === text) {
224
393
  return;
@@ -36,15 +36,6 @@ function parseSchedule(input) {
36
36
  ...(input.tz ? { tz: input.tz } : {}),
37
37
  };
38
38
  }
39
- function parseSessionPolicy(value) {
40
- if (value === undefined) {
41
- return undefined;
42
- }
43
- if (value === "stateless" || value === "shared-session") {
44
- return value;
45
- }
46
- throw new Error(`Invalid session policy '${value}'. Expected 'stateless' or 'shared-session'.`);
47
- }
48
39
  function assertDeliveryReferences(config, input) {
49
40
  if (!config.connectors.items[input.connectorId]) {
50
41
  throw new Error(`Unknown connectorId '${input.connectorId}'`);
@@ -86,13 +77,11 @@ export async function runCronAddCommand(options) {
86
77
  const now = Date.now();
87
78
  const nextRunAtMs = computeInitialNextRunAtMs(schedule, now);
88
79
  const id = `${slugify(options.name)}-${Math.random().toString(36).slice(2, 8)}`;
89
- const sessionPolicy = parseSessionPolicy(options.sessionPolicy) ?? "stateless";
90
80
  const job = {
91
81
  id,
92
82
  name: options.name,
93
83
  enabled: true,
94
84
  schedule,
95
- sessionPolicy,
96
85
  prompt: options.prompt,
97
86
  delivery: {
98
87
  connectorId: options.connectorId,
@@ -132,6 +121,9 @@ export async function runCronListCommand(options) {
132
121
  console.log(`- ${job.id} [${job.enabled ? "enabled" : "paused"}] ${job.name}`);
133
122
  console.log(` schedule=${schedule}`);
134
123
  console.log(` next=${next} last=${last} status=${job.state.lastStatus ?? "-"}`);
124
+ if (job.state.manualRunRequestedAtMs !== undefined) {
125
+ console.log(` manualRun=${new Date(job.state.manualRunRequestedAtMs).toISOString()}`);
126
+ }
135
127
  console.log(` delivery=${job.delivery.connectorId}/${job.delivery.routeId}/${job.delivery.channelId}`);
136
128
  }
137
129
  }
@@ -154,10 +146,12 @@ export async function runCronStatusCommand(options) {
154
146
  console.log(`- name: ${target.name}`);
155
147
  console.log(`- enabled: ${target.enabled}`);
156
148
  console.log(`- schedule: ${describeSchedule(target.schedule)}`);
157
- console.log(`- sessionPolicy: ${target.sessionPolicy ?? "stateless"}`);
158
149
  console.log(`- nextRun: ${target.state.nextRunAtMs ? new Date(target.state.nextRunAtMs).toISOString() : "-"}`);
159
150
  console.log(`- lastRun: ${target.state.lastRunAtMs ? new Date(target.state.lastRunAtMs).toISOString() : "-"}`);
160
151
  console.log(`- lastStatus: ${target.state.lastStatus ?? "-"}`);
152
+ if (target.state.manualRunRequestedAtMs !== undefined) {
153
+ console.log(`- manualRunQueuedAt: ${new Date(target.state.manualRunRequestedAtMs).toISOString()}`);
154
+ }
161
155
  if (target.state.lastError) {
162
156
  console.log(`- lastError: ${target.state.lastError}`);
163
157
  }
@@ -173,28 +167,23 @@ export async function runCronRunCommand(options) {
173
167
  const now = Date.now();
174
168
  await context.store.updateJob(options.jobId, (current) => ({
175
169
  ...current,
176
- enabled: true,
177
170
  updatedAtMs: now,
178
171
  state: {
179
172
  ...current.state,
180
- nextRunAtMs: now,
173
+ manualRunRequestedAtMs: current.state.manualRunRequestedAtMs ?? now,
181
174
  },
182
175
  }));
183
- console.log(`Scheduled cron job ${options.jobId} to run on next scheduler tick.`);
176
+ console.log(`Queued one manual run for cron job ${options.jobId}.`);
177
+ console.log("It will execute once without changing the job's enabled state or next scheduled run.");
184
178
  console.log("Ensure the gateway process is running for execution.");
185
179
  }
186
180
  export async function runCronUpdateCommand(options) {
187
181
  const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
188
182
  const now = Date.now();
189
- const hasScheduleUpdate = options.at !== undefined || options.everyMs !== undefined || options.cronExpr !== undefined;
190
- const nextSchedule = hasScheduleUpdate
191
- ? parseSchedule({
192
- ...(options.at ? { at: options.at } : {}),
193
- ...(options.everyMs !== undefined ? { everyMs: options.everyMs } : {}),
194
- ...(options.cronExpr ? { cronExpr: options.cronExpr } : {}),
195
- ...(options.tz ? { tz: options.tz } : {}),
196
- })
197
- : null;
183
+ const hasScheduleUpdate = options.at !== undefined
184
+ || options.everyMs !== undefined
185
+ || options.cronExpr !== undefined
186
+ || options.tz !== undefined;
198
187
  await context.store.updateJob(options.jobId, (current) => {
199
188
  const updatedDelivery = {
200
189
  connectorId: options.connectorId ?? current.delivery.connectorId,
@@ -209,14 +198,37 @@ export async function runCronUpdateCommand(options) {
209
198
  connectorId: updatedDelivery.connectorId,
210
199
  routeId: updatedDelivery.routeId,
211
200
  });
212
- const schedule = nextSchedule ?? current.schedule;
213
- const nextRunAtMs = nextSchedule
201
+ let schedule = current.schedule;
202
+ if (hasScheduleUpdate) {
203
+ const hasExplicitScheduleVariant = options.at !== undefined
204
+ || options.everyMs !== undefined
205
+ || options.cronExpr !== undefined;
206
+ if (hasExplicitScheduleVariant) {
207
+ schedule = parseSchedule({
208
+ ...(options.at ? { at: options.at } : {}),
209
+ ...(options.everyMs !== undefined ? { everyMs: options.everyMs } : {}),
210
+ ...(options.cronExpr ? { cronExpr: options.cronExpr } : {}),
211
+ ...(options.tz ? { tz: options.tz } : {}),
212
+ });
213
+ }
214
+ else if (options.tz !== undefined) {
215
+ if (current.schedule.kind !== "cron") {
216
+ throw new Error("--tz can only be updated for cron schedules or together with --cron");
217
+ }
218
+ schedule = {
219
+ kind: "cron",
220
+ expr: current.schedule.expr,
221
+ ...(options.tz ? { tz: options.tz } : {}),
222
+ };
223
+ }
224
+ }
225
+ const nextRunAtMs = hasScheduleUpdate
214
226
  ? computeInitialNextRunAtMs(schedule, now)
215
227
  : current.state.nextRunAtMs;
216
228
  const nextState = {
217
229
  ...current.state,
218
230
  };
219
- if (nextSchedule) {
231
+ if (hasScheduleUpdate) {
220
232
  if (nextRunAtMs === undefined) {
221
233
  delete nextState.nextRunAtMs;
222
234
  }
@@ -224,7 +236,6 @@ export async function runCronUpdateCommand(options) {
224
236
  nextState.nextRunAtMs = nextRunAtMs;
225
237
  }
226
238
  }
227
- const parsedSessionPolicy = parseSessionPolicy(options.sessionPolicy);
228
239
  const nextJob = {
229
240
  ...current,
230
241
  name: options.name ?? current.name,
@@ -234,13 +245,6 @@ export async function runCronUpdateCommand(options) {
234
245
  updatedAtMs: now,
235
246
  state: nextState,
236
247
  };
237
- const nextSessionPolicy = parsedSessionPolicy ?? current.sessionPolicy;
238
- if (nextSessionPolicy !== undefined) {
239
- nextJob.sessionPolicy = nextSessionPolicy;
240
- }
241
- else {
242
- delete nextJob.sessionPolicy;
243
- }
244
248
  return nextJob;
245
249
  });
246
250
  console.log(`Updated cron job ${options.jobId}`);