@dobby.ai/dobby 0.1.1 → 0.2.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 (136) hide show
  1. package/README.md +72 -7
  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/program.js +0 -6
  5. package/dist/src/core/types.js +2 -0
  6. package/dist/src/cron/config.js +2 -2
  7. package/dist/src/cron/service.js +87 -23
  8. package/dist/src/cron/store.js +1 -1
  9. package/package.json +9 -3
  10. package/.env.example +0 -8
  11. package/AGENTS.md +0 -267
  12. package/ROADMAP.md +0 -34
  13. package/config/cron.example.json +0 -9
  14. package/config/gateway.example.json +0 -132
  15. package/dist/plugins/connector-discord/src/mapper.js +0 -75
  16. package/dist/src/cli/tests/config-command.test.js +0 -42
  17. package/dist/src/cli/tests/config-io.test.js +0 -64
  18. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  19. package/dist/src/cli/tests/discord-mapper.test.js +0 -90
  20. package/dist/src/cli/tests/doctor.test.js +0 -252
  21. package/dist/src/cli/tests/init-catalog.test.js +0 -134
  22. package/dist/src/cli/tests/program-options.test.js +0 -78
  23. package/dist/src/cli/tests/routing-config.test.js +0 -254
  24. package/dist/src/core/tests/control-command.test.js +0 -17
  25. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  26. package/dist/src/core/tests/typing-controller.test.js +0 -103
  27. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  28. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  29. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  30. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  31. package/docs/MVP.md +0 -135
  32. package/docs/RUNBOOK.md +0 -243
  33. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  34. package/plugins/connector-discord/dobby.manifest.json +0 -18
  35. package/plugins/connector-discord/index.js +0 -1
  36. package/plugins/connector-discord/package-lock.json +0 -360
  37. package/plugins/connector-discord/package.json +0 -38
  38. package/plugins/connector-discord/src/connector.ts +0 -345
  39. package/plugins/connector-discord/src/contribution.ts +0 -21
  40. package/plugins/connector-discord/src/mapper.ts +0 -101
  41. package/plugins/connector-discord/tsconfig.json +0 -19
  42. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  43. package/plugins/connector-feishu/index.js +0 -1
  44. package/plugins/connector-feishu/package-lock.json +0 -618
  45. package/plugins/connector-feishu/package.json +0 -38
  46. package/plugins/connector-feishu/src/connector.ts +0 -343
  47. package/plugins/connector-feishu/src/contribution.ts +0 -26
  48. package/plugins/connector-feishu/src/mapper.ts +0 -401
  49. package/plugins/connector-feishu/tsconfig.json +0 -19
  50. package/plugins/plugin-sdk/index.d.ts +0 -261
  51. package/plugins/plugin-sdk/index.js +0 -1
  52. package/plugins/plugin-sdk/package-lock.json +0 -12
  53. package/plugins/plugin-sdk/package.json +0 -22
  54. package/plugins/provider-claude/dobby.manifest.json +0 -17
  55. package/plugins/provider-claude/index.js +0 -1
  56. package/plugins/provider-claude/package-lock.json +0 -3398
  57. package/plugins/provider-claude/package.json +0 -39
  58. package/plugins/provider-claude/src/contribution.ts +0 -1018
  59. package/plugins/provider-claude/tsconfig.json +0 -19
  60. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  61. package/plugins/provider-claude-cli/index.js +0 -1
  62. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  63. package/plugins/provider-claude-cli/package.json +0 -38
  64. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  65. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  66. package/plugins/provider-pi/dobby.manifest.json +0 -17
  67. package/plugins/provider-pi/index.js +0 -1
  68. package/plugins/provider-pi/package-lock.json +0 -3877
  69. package/plugins/provider-pi/package.json +0 -40
  70. package/plugins/provider-pi/src/contribution.ts +0 -606
  71. package/plugins/provider-pi/tsconfig.json +0 -19
  72. package/plugins/sandbox-core/boxlite.js +0 -1
  73. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  74. package/plugins/sandbox-core/docker.js +0 -1
  75. package/plugins/sandbox-core/package-lock.json +0 -136
  76. package/plugins/sandbox-core/package.json +0 -39
  77. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  78. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  79. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  80. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  81. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  82. package/plugins/sandbox-core/tsconfig.json +0 -19
  83. package/scripts/local-extensions.mjs +0 -168
  84. package/src/agent/event-forwarder.ts +0 -414
  85. package/src/cli/commands/config.ts +0 -328
  86. package/src/cli/commands/configure.ts +0 -92
  87. package/src/cli/commands/cron.ts +0 -410
  88. package/src/cli/commands/doctor.ts +0 -331
  89. package/src/cli/commands/extension.ts +0 -207
  90. package/src/cli/commands/init.ts +0 -211
  91. package/src/cli/commands/start.ts +0 -223
  92. package/src/cli/commands/topology.ts +0 -415
  93. package/src/cli/index.ts +0 -9
  94. package/src/cli/program.ts +0 -314
  95. package/src/cli/shared/config-io.ts +0 -245
  96. package/src/cli/shared/config-mutators.ts +0 -470
  97. package/src/cli/shared/config-schema.ts +0 -228
  98. package/src/cli/shared/config-types.ts +0 -129
  99. package/src/cli/shared/configure-sections.ts +0 -595
  100. package/src/cli/shared/discord-config.ts +0 -14
  101. package/src/cli/shared/init-catalog.ts +0 -249
  102. package/src/cli/shared/local-extension-specs.ts +0 -108
  103. package/src/cli/shared/runtime.ts +0 -33
  104. package/src/cli/shared/schema-prompts.ts +0 -443
  105. package/src/cli/tests/config-command.test.ts +0 -56
  106. package/src/cli/tests/config-io.test.ts +0 -92
  107. package/src/cli/tests/config-mutators.test.ts +0 -59
  108. package/src/cli/tests/discord-mapper.test.ts +0 -128
  109. package/src/cli/tests/doctor.test.ts +0 -269
  110. package/src/cli/tests/init-catalog.test.ts +0 -144
  111. package/src/cli/tests/program-options.test.ts +0 -95
  112. package/src/cli/tests/routing-config.test.ts +0 -281
  113. package/src/core/control-command.ts +0 -12
  114. package/src/core/dedup-store.ts +0 -103
  115. package/src/core/gateway.ts +0 -609
  116. package/src/core/routing.ts +0 -404
  117. package/src/core/runtime-registry.ts +0 -141
  118. package/src/core/tests/control-command.test.ts +0 -20
  119. package/src/core/tests/runtime-registry.test.ts +0 -140
  120. package/src/core/tests/typing-controller.test.ts +0 -129
  121. package/src/core/types.ts +0 -324
  122. package/src/core/typing-controller.ts +0 -119
  123. package/src/cron/config.ts +0 -154
  124. package/src/cron/schedule.ts +0 -61
  125. package/src/cron/service.ts +0 -249
  126. package/src/cron/store.ts +0 -155
  127. package/src/cron/types.ts +0 -60
  128. package/src/extension/loader.ts +0 -145
  129. package/src/extension/manager.ts +0 -355
  130. package/src/extension/manifest.ts +0 -26
  131. package/src/extension/registry.ts +0 -229
  132. package/src/main.ts +0 -8
  133. package/src/sandbox/executor.ts +0 -44
  134. package/src/sandbox/host-executor.ts +0 -118
  135. package/src/shared/dobby-repo.ts +0 -48
  136. 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`
@@ -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
  ## 快速开始
@@ -213,6 +214,37 @@ dobby cron resume <jobId>
213
214
  dobby cron remove <jobId>
214
215
  ```
215
216
 
217
+ ## Release 流程
218
+
219
+ 仓库现在内置了两条 GitHub Actions:
220
+
221
+ - `.github/workflows/ci.yml`
222
+ - 在 PR / push 到 `main` 时执行 `npm ci`、`npm run plugins:install`、`npm run check`、`npm run build`、`npm run test:cli`、`npm run plugins:check`、`npm run plugins:build`
223
+ - `.github/workflows/release.yml`
224
+ - 在 push 到 `main` 时运行 Release Please
225
+ - 有 releasable commit 时自动维护 release PR
226
+ - release PR 合并后自动发布对应 npm 包,并为每个包生成独立 GitHub release / tag
227
+
228
+ 推荐的日常流程:
229
+
230
+ 1. 正常提交功能改动到 PR(建议继续使用 Conventional Commits 风格,例如 `feat(...)` / `fix(...)`)
231
+ 2. 合并到 `main`
232
+ 3. 等待 Release Please 自动更新或创建 release PR
233
+ 4. review 并合并 release PR
234
+ 5. 合并后由 `release.yml` 自动执行 npm trusted publishing
235
+
236
+ 注意:
237
+
238
+ - 首次启用前,需要在 npm 后台为每个 `@dobby.ai/*` 包配置 GitHub trusted publisher,指向当前仓库和 `.github/workflows/release.yml`
239
+ - 建议在 GitHub 仓库里创建 `npm-publish` environment,后续若需要人工审批可以直接加保护规则
240
+ - 进入自动发版流程后,后续版本号应由 Release Please 维护,不再手动执行 `npm version`
241
+ - 本地手动兜底发布仍然保留,可用:
242
+
243
+ ```bash
244
+ node scripts/publish-packages.mjs --package plugins/provider-codex-cli --skip-existing
245
+ node scripts/publish-packages.mjs --package . --tag next
246
+ ```
247
+
216
248
  ## Gateway 配置模型
217
249
 
218
250
  顶层结构:
@@ -245,10 +277,6 @@ dobby cron remove <jobId>
245
277
  - 未指定时默认使用 `host.builtin`
246
278
  - 未匹配 binding 的入站消息会被直接忽略;仅 direct message 可回落到 `bindings.default`
247
279
 
248
- 当前代码还保留但未真正生效的字段:
249
-
250
- - cron job 的 `sessionPolicy`
251
-
252
280
  示例配置:
253
281
 
254
282
  - gateway:[`config/gateway.example.json`](config/gateway.example.json)
@@ -278,6 +306,7 @@ dobby cron remove <jobId>
278
306
 
279
307
  - `connector.discord`
280
308
  - `provider.pi`
309
+ - `provider.codex-cli`
281
310
  - `provider.claude-cli`
282
311
  - `provider.claude`
283
312
  - `sandbox.boxlite`
@@ -288,13 +317,49 @@ dobby cron remove <jobId>
288
317
  - provider:`provider.pi`、`provider.claude-cli`
289
318
  - connector:`connector.discord`、`connector.feishu`
290
319
 
291
- `provider.claude` 与 sandbox 相关扩展需要手工安装和配置,例如:
320
+ `provider.codex-cli`、`provider.claude` 与 sandbox 相关扩展需要手工安装和配置,例如:
292
321
 
293
322
  ```bash
323
+ npm run start -- extension install @dobby.ai/provider-codex-cli --enable
294
324
  npm run start -- extension install @dobby.ai/provider-claude --enable
295
325
  npm run start -- extension install @dobby.ai/sandbox-core --enable
296
326
  ```
297
327
 
328
+ 若使用 `provider.codex-cli`,启动前建议检查:
329
+
330
+ ```bash
331
+ codex --version
332
+ codex login status
333
+ ```
334
+
335
+ 最小配置字段:
336
+
337
+ - `command`(默认 `codex`)
338
+ - `commandArgs`(默认 `[]`)
339
+ - `model`(可选;未设置时沿用 Codex CLI 当前 profile / `~/.codex/config.toml` 的默认模型)
340
+ - `profile`(可选;等价于 `codex -p <profile>`)
341
+ - `approvalPolicy`(可选;默认 `never`)
342
+ - `sandboxMode`(可选;不填时按 route 的 `tools` 推导:`readonly -> read-only`,`full -> workspace-write`)
343
+ - `configOverrides`(可选;字符串数组,按原样转成重复的 `codex -c key=value`)
344
+ - `skipGitRepoCheck`(默认 `false`)
345
+
346
+ 例如希望网关里的 Codex 会话复用本机 profile,并显式打开无人值守执行:
347
+
348
+ ```json
349
+ {
350
+ "type": "provider.codex-cli",
351
+ "command": "codex",
352
+ "profile": "background",
353
+ "approvalPolicy": "never",
354
+ "sandboxMode": "danger-full-access",
355
+ "configOverrides": [
356
+ "model_reasoning_effort = \"xhigh\""
357
+ ]
358
+ }
359
+ ```
360
+
361
+ 注意:`provider.codex-cli` 当前是 host-only,`danger-full-access` 会直接作用在宿主机上。
362
+
298
363
  `--enable` 的行为:
299
364
 
300
365
  - 把包写入 `extensions.allowList`
@@ -323,7 +388,7 @@ npm run start -- cron add daily-report \
323
388
 
324
389
  说明:
325
390
 
326
- - `cron run <jobId>` 只是把 job 标记为“下一次 scheduler tick 执行”
391
+ - `cron run <jobId>` 会额外排队一次立即执行,不会恢复 paused 状态,也不会改写原有 `nextRunAtMs`
327
392
  - 需要已有一个正在运行的 `dobby start`
328
393
  - 当前 scheduled run 一律按 stateless / ephemeral 执行
329
394
 
@@ -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}`);
@@ -137,7 +137,6 @@ export function buildProgram() {
137
137
  .requiredOption("--route <id>", "Route ID")
138
138
  .requiredOption("--channel <id>", "Delivery channel/chat ID")
139
139
  .option("--thread <id>", "Delivery thread ID")
140
- .option("--session-policy <policy>", "Session policy: stateless|shared-session", "stateless")
141
140
  .option("--at <iso>", "Run once at ISO timestamp")
142
141
  .option("--every-ms <ms>", "Run at fixed interval in milliseconds")
143
142
  .option("--cron <expr>", "Cron expression")
@@ -152,7 +151,6 @@ export function buildProgram() {
152
151
  routeId: opts.route,
153
152
  channelId: opts.channel,
154
153
  ...(typeof opts.thread === "string" ? { threadId: opts.thread } : {}),
155
- sessionPolicy: opts.sessionPolicy,
156
154
  ...(typeof opts.at === "string" ? { at: opts.at } : {}),
157
155
  ...(parsedEveryMs !== null && Number.isFinite(parsedEveryMs) ? { everyMs: parsedEveryMs } : {}),
158
156
  ...(typeof opts.cron === "string" ? { cronExpr: opts.cron } : {}),
@@ -206,7 +204,6 @@ export function buildProgram() {
206
204
  .option("--channel <id>", "Delivery channel/chat ID")
207
205
  .option("--thread <id>", "Delivery thread ID")
208
206
  .option("--clear-thread", "Unset delivery thread", false)
209
- .option("--session-policy <policy>", "Session policy: stateless|shared-session")
210
207
  .option("--at <iso>", "Run once at ISO timestamp")
211
208
  .option("--every-ms <ms>", "Run at fixed interval in milliseconds")
212
209
  .option("--cron <expr>", "Cron expression")
@@ -223,9 +220,6 @@ export function buildProgram() {
223
220
  ...(typeof opts.channel === "string" ? { channelId: opts.channel } : {}),
224
221
  ...(typeof opts.thread === "string" ? { threadId: opts.thread } : {}),
225
222
  clearThread: Boolean(opts.clearThread),
226
- ...(typeof opts.sessionPolicy === "string"
227
- ? { sessionPolicy: opts.sessionPolicy }
228
- : {}),
229
223
  ...(typeof opts.at === "string" ? { at: opts.at } : {}),
230
224
  ...(parsedEveryMs !== null && Number.isFinite(parsedEveryMs) ? { everyMs: parsedEveryMs } : {}),
231
225
  ...(typeof opts.cron === "string" ? { cronExpr: opts.cron } : {}),
@@ -1 +1,3 @@
1
1
  export const BUILTIN_HOST_SANDBOX_ID = "host.builtin";
2
+ export const OUTBOUND_MESSAGE_KIND_METADATA_KEY = "dobby_message_kind";
3
+ export const OUTBOUND_MESSAGE_KIND_PROGRESS = "progress";
@@ -7,7 +7,7 @@ const rawCronConfigSchema = z.object({
7
7
  storeFile: z.string().min(1).optional(),
8
8
  runLogFile: z.string().min(1).optional(),
9
9
  pollIntervalMs: z.number().int().positive().default(10_000),
10
- maxConcurrentRuns: z.number().int().positive().default(1),
10
+ maxConcurrentRuns: z.number().int().positive().default(2),
11
11
  runMissedOnStartup: z.boolean().default(true),
12
12
  jobTimeoutMs: z.number().int().positive().default(10 * 60 * 1000),
13
13
  });
@@ -45,7 +45,7 @@ function defaultCronConfigPayload() {
45
45
  storeFile: "./data/state/cron-jobs.json",
46
46
  runLogFile: "./data/state/cron-runs.jsonl",
47
47
  pollIntervalMs: 10_000,
48
- maxConcurrentRuns: 1,
48
+ maxConcurrentRuns: 2,
49
49
  runMissedOnStartup: true,
50
50
  jobTimeoutMs: 10 * 60 * 1000,
51
51
  };