@dobby.ai/dobby 0.1.1 → 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 (136) hide show
  1. package/README.md +20 -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
  ## 快速开始
@@ -245,10 +246,6 @@ dobby cron remove <jobId>
245
246
  - 未指定时默认使用 `host.builtin`
246
247
  - 未匹配 binding 的入站消息会被直接忽略;仅 direct message 可回落到 `bindings.default`
247
248
 
248
- 当前代码还保留但未真正生效的字段:
249
-
250
- - cron job 的 `sessionPolicy`
251
-
252
249
  示例配置:
253
250
 
254
251
  - gateway:[`config/gateway.example.json`](config/gateway.example.json)
@@ -278,6 +275,7 @@ dobby cron remove <jobId>
278
275
 
279
276
  - `connector.discord`
280
277
  - `provider.pi`
278
+ - `provider.codex-cli`
281
279
  - `provider.claude-cli`
282
280
  - `provider.claude`
283
281
  - `sandbox.boxlite`
@@ -288,13 +286,28 @@ dobby cron remove <jobId>
288
286
  - provider:`provider.pi`、`provider.claude-cli`
289
287
  - connector:`connector.discord`、`connector.feishu`
290
288
 
291
- `provider.claude` 与 sandbox 相关扩展需要手工安装和配置,例如:
289
+ `provider.codex-cli`、`provider.claude` 与 sandbox 相关扩展需要手工安装和配置,例如:
292
290
 
293
291
  ```bash
292
+ npm run start -- extension install @dobby.ai/provider-codex-cli --enable
294
293
  npm run start -- extension install @dobby.ai/provider-claude --enable
295
294
  npm run start -- extension install @dobby.ai/sandbox-core --enable
296
295
  ```
297
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
+
298
311
  `--enable` 的行为:
299
312
 
300
313
  - 把包写入 `extensions.allowList`
@@ -323,7 +336,7 @@ npm run start -- cron add daily-report \
323
336
 
324
337
  说明:
325
338
 
326
- - `cron run <jobId>` 只是把 job 标记为“下一次 scheduler tick 执行”
339
+ - `cron run <jobId>` 会额外排队一次立即执行,不会恢复 paused 状态,也不会改写原有 `nextRunAtMs`
327
340
  - 需要已有一个正在运行的 `dobby start`
328
341
  - 当前 scheduled run 一律按 stateless / ephemeral 执行
329
342
 
@@ -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
  };