@kafca/agentdock 0.1.35 → 0.1.37

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 (63) hide show
  1. package/README.md +7 -0
  2. package/dist/renderer/assets/{Badge-BM4WCcua.js → Badge-CjzZp9Tf.js} +1 -1
  3. package/dist/renderer/assets/{Chat-DoyBHOsx.js → Chat-D4_5K_Ww.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-BWj04kgW.js → ChatMarkdown-DkI29dOw.js} +1 -1
  5. package/dist/renderer/assets/{Config-A95OuazQ.js → Config-CnrlmSEQ.js} +1 -1
  6. package/dist/renderer/assets/{CronList-DsPU4zGT.js → CronList-DVvInMvr.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-D8Vpj97x.js → Dashboard-CY4YAmVj.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-x1uVoSxZ.js → EmptyState-CmR947i4.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-C9TbcyNJ.js → KnowledgeDetail-BvNG0inb.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-Cg9Fb41Q.js → KnowledgeHome-BB0Z8VrS.js} +1 -1
  11. package/dist/renderer/assets/{Logs-oWaKA60u.js → Logs-IORTjnGM.js} +1 -1
  12. package/dist/renderer/assets/{Modal-24hTXXHK.js → Modal-D9NZ6cvF.js} +1 -1
  13. package/dist/renderer/assets/{Page-YRtQonKZ.js → Page-DlvElfTz.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-DScsxJO-.js → ProjectDetail-Y1ZFL8W9.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-8L5GD7PN.js → ProjectList-DAI-4HU7.js} +1 -1
  16. package/dist/renderer/assets/{Select-BI8Uw_DF.js → Select-LMEt9yJy.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-BIoCWdDD.js → SessionChat-BWRAS_H-.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-BsEJeDxI.js → SessionList-DQsxmC1n.js} +1 -1
  19. package/dist/renderer/assets/{ThreadChat-DJi0UBta.js → ThreadChat-D9j78cdm.js} +1 -1
  20. package/dist/renderer/assets/{Workspace-tlJxrgC5.js → Workspace-C-RTj4uI.js} +1 -1
  21. package/dist/renderer/assets/{arrow-left-pzaBhi9y.js → arrow-left-C_ZIHUlD.js} +1 -1
  22. package/dist/renderer/assets/{book-open-CNhlVyqh.js → book-open-BuI3webp.js} +1 -1
  23. package/dist/renderer/assets/{check-Cg2LLCdl.js → check-FA1rU94Q.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-Dgqg07wf.js → chevron-down-DvQ7Hq2s.js} +1 -1
  25. package/dist/renderer/assets/{circle-BBd_jIS2.js → circle-DZYvZ-Iq.js} +1 -1
  26. package/dist/renderer/assets/{desktop-C0S-Yv8G.js → desktop-BSoKBjFu.js} +1 -1
  27. package/dist/renderer/assets/{funnel-DwSh-SrX.js → funnel-BoW8A8Fi.js} +1 -1
  28. package/dist/renderer/assets/{heart-CWVc-KhZ.js → heart-DaIdiPUs.js} +1 -1
  29. package/dist/renderer/assets/{index-DZbeAYVk.js → index-Cc83IKng.js} +2 -2
  30. package/dist/renderer/assets/{index-HYmDNQ9k.js → index-pdvxKnBr.js} +1 -1
  31. package/dist/renderer/assets/{pencil-tI4tCLz0.js → pencil-CMs10l_8.js} +1 -1
  32. package/dist/renderer/assets/{play-Coi2HA1w.js → play-DtT_FrBZ.js} +1 -1
  33. package/dist/renderer/assets/{plug-D4TCyW9b.js → plug-vKGMXapD.js} +1 -1
  34. package/dist/renderer/assets/{plus-p62AgpoB.js → plus-DZV73wzr.js} +1 -1
  35. package/dist/renderer/assets/projects-BUda9SpJ.js +1 -0
  36. package/dist/renderer/assets/{save-fpMhkVV0.js → save-Bus9w20l.js} +1 -1
  37. package/dist/renderer/assets/{search-BHKIStdA.js → search-ChEC20LW.js} +1 -1
  38. package/dist/renderer/assets/{server-0oTWed8U.js → server-B9HgN6CF.js} +1 -1
  39. package/dist/renderer/assets/{session-utils-D0Qv0I0-.js → session-utils-Bn9-xCic.js} +1 -1
  40. package/dist/renderer/assets/{sessions-C8ApKQgC.js → sessions-RUHmhxT5.js} +1 -1
  41. package/dist/renderer/assets/{shield-check-BAJI9jzC.js → shield-check-B97md1a5.js} +1 -1
  42. package/dist/renderer/assets/{trash-2-B-Ygm-x6.js → trash-2-31WOGl8I.js} +1 -1
  43. package/dist/renderer/assets/{user-Cv8rzw8l.js → user-CLiiHYE_.js} +1 -1
  44. package/dist/renderer/assets/{wifi-off-BImS3nkm.js → wifi-off-BKh21npc.js} +1 -1
  45. package/dist/renderer/index.html +1 -1
  46. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +22 -7
  47. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +2 -1
  48. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +37 -7
  49. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +31 -1
  50. package/dist-electron/services/local-ai-core/src/scheduler/channel-execution-policy.js +79 -0
  51. package/dist-electron/services/local-ai-core/src/scheduler/lark-execution-policies.js +7 -61
  52. package/dist-electron/services/local-ai-core/src/scheduler/lark-schedule-adapter.js +4 -13
  53. package/dist-electron/services/local-ai-core/src/scheduler/local-schedule-adapter.js +23 -30
  54. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-bridge-session.js +27 -0
  55. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +3 -2
  56. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-run-lifecycle.js +13 -1
  57. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +1 -1
  58. package/dist-electron/services/local-ai-core/src/scheduler/weixin-execution-policies.js +7 -61
  59. package/dist-electron/services/local-ai-core/src/scheduler/weixin-schedule-adapter.js +4 -13
  60. package/dist-electron/tests/electron/workspace-task-store.test.js +244 -24
  61. package/dist-electron/tests/integration/local-core-refactor.test.js +60 -7
  62. package/package.json +1 -1
  63. package/dist/renderer/assets/projects-CTDuVxeQ.js +0 -1
@@ -1,34 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalScheduleAdapter = void 0;
4
- const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'interrupted']);
5
- const SCHEDULED_RUN_PERMISSION_MODE = 'bypassPermissions';
4
+ const scheduled_conversation_executor_js_1 = require("./scheduled-conversation-executor.js");
6
5
  class LocalScheduleAdapter {
7
6
  options;
7
+ executor;
8
8
  deliveryTargets = ['local'];
9
9
  constructor(options) {
10
10
  this.options = options;
11
+ this.executor = new scheduled_conversation_executor_js_1.ScheduledConversationExecutor({
12
+ store: options.store,
13
+ getWorkspaceRouter: options.getWorkspaceRouter,
14
+ });
11
15
  }
12
16
  supports(job) {
13
17
  return job.platform === 'local' && (job.route.type === 'local.thread' || job.route.type === 'thread');
14
18
  }
15
19
  async execute(context) {
16
20
  const { job } = context;
17
- const workspaceRouter = this.options.getWorkspaceRouter();
18
- const threadId = await this.resolveThread(job);
19
- const sendResult = await workspaceRouter.sendThreadMessage(threadId, job.promptTemplate, {
20
- permissionMode: SCHEDULED_RUN_PERMISSION_MODE,
21
- });
22
- await this.waitForRun(sendResult.runId);
23
- const thread = await workspaceRouter.getThread(threadId);
24
- const replyText = [...thread.messages]
25
- .reverse()
26
- .find((message) => message.role === 'assistant' && message.kind === 'final')
27
- ?.content;
21
+ const execution = await this.executor.execute(job, job.promptTemplate, this.createExecutionPolicy());
22
+ return {
23
+ threadId: execution.threadId,
24
+ runId: execution.runId,
25
+ replyText: execution.replyText,
26
+ deliveryMode: 'thread-only',
27
+ deliveryStatus: 'succeeded',
28
+ };
29
+ }
30
+ createExecutionPolicy() {
28
31
  return {
29
- threadId,
30
- runId: sendResult.runId,
31
- replyText,
32
+ resolveTarget: async (job) => ({
33
+ kind: 'local:thread',
34
+ threadId: await this.resolveThread(job),
35
+ workspaceId: job.workspaceId,
36
+ platform: job.platform,
37
+ route: job.route,
38
+ }),
32
39
  };
33
40
  }
34
41
  async resolveThread(job) {
@@ -54,19 +61,5 @@ class LocalScheduleAdapter {
54
61
  return false;
55
62
  }
56
63
  }
57
- async waitForRun(runId, timeoutMs = 15 * 60 * 1000) {
58
- const startedAt = Date.now();
59
- while (Date.now() - startedAt < timeoutMs) {
60
- const run = this.options.store.getRun(runId);
61
- if (run && TERMINAL_RUN_STATES.has(run.status)) {
62
- if (run.status !== 'completed') {
63
- throw new Error(`Scheduled run finished with status ${run.status}`);
64
- }
65
- return;
66
- }
67
- await new Promise((resolve) => setTimeout(resolve, 300));
68
- }
69
- throw new Error(`Timed out waiting for scheduled run ${runId}`);
70
- }
71
64
  }
72
65
  exports.LocalScheduleAdapter = LocalScheduleAdapter;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScheduledBridgeSession = void 0;
4
+ class ScheduledBridgeSession {
5
+ static async open(input) {
6
+ const sessionKey = input.workspaceRouter.getThreadSessionKey(input.threadId);
7
+ const unregister = await input.getChannelRuntime().registerScheduledThreadBridge?.({
8
+ workspaceId: input.job.workspaceId,
9
+ platform: input.job.platform,
10
+ route: input.job.route,
11
+ threadId: input.threadId,
12
+ sessionKey,
13
+ });
14
+ return {
15
+ mode: 'bridge-stream',
16
+ workspaceId: input.job.workspaceId,
17
+ platform: input.job.platform,
18
+ route: input.job.route,
19
+ threadId: input.threadId,
20
+ sessionKey,
21
+ close: async () => {
22
+ await unregister?.();
23
+ },
24
+ };
25
+ }
26
+ }
27
+ exports.ScheduledBridgeSession = ScheduledBridgeSession;
@@ -13,12 +13,13 @@ class ScheduledConversationExecutor {
13
13
  const target = await policy.resolveTarget(job);
14
14
  await policy.beforeExecute?.(target, job);
15
15
  try {
16
- const sendResult = await this.options.workspaceRouter.sendThreadMessage(target.threadId, prompt, {
16
+ const workspaceRouter = this.options.getWorkspaceRouter();
17
+ const sendResult = await workspaceRouter.sendThreadMessage(target.threadId, prompt, {
17
18
  permissionMode: SCHEDULED_RUN_PERMISSION_MODE,
18
19
  runtimeEnv: this.buildScheduledRuntimeEnv(target),
19
20
  });
20
21
  await this.waitForRun(sendResult.runId, timeoutMs);
21
- const thread = await this.options.workspaceRouter.getThread(target.threadId);
22
+ const thread = await workspaceRouter.getThread(target.threadId);
22
23
  const replyText = [...thread.messages]
23
24
  .reverse()
24
25
  .find((message) => message.role === 'assistant' && message.kind === 'final')
@@ -10,13 +10,18 @@ class SchedulerRunLifecycle {
10
10
  const skipped = this.options.store.createScheduledJobRun(job.id, 'skipped', {
11
11
  triggeredAt,
12
12
  error,
13
+ deliveryStatus: 'skipped',
14
+ deliveryError: error,
13
15
  });
14
16
  this.options.emitRun(skipped);
15
17
  this.emitCurrentJob(job.id);
16
18
  return skipped;
17
19
  }
18
20
  markQueued(job, triggeredAt) {
19
- const run = this.options.store.createScheduledJobRun(job.id, 'queued', { triggeredAt });
21
+ const run = this.options.store.createScheduledJobRun(job.id, 'queued', {
22
+ triggeredAt,
23
+ deliveryStatus: 'pending',
24
+ });
20
25
  this.options.emitRun(run);
21
26
  return run;
22
27
  }
@@ -35,6 +40,11 @@ class SchedulerRunLifecycle {
35
40
  threadId: result.threadId,
36
41
  runId: result.runId,
37
42
  platformMessageId: result.platformMessageId,
43
+ platformMessageIds: result.platformMessageIds,
44
+ deliveryMode: result.deliveryMode,
45
+ deliveryStatus: result.deliveryStatus || 'succeeded',
46
+ deliveryError: result.deliveryError || '',
47
+ lastBridgeEventAt: result.lastBridgeEventAt,
38
48
  error: '',
39
49
  });
40
50
  if (disableOnceJob) {
@@ -49,6 +59,8 @@ class SchedulerRunLifecycle {
49
59
  status: 'failed',
50
60
  finishedAt: new Date().toISOString(),
51
61
  error,
62
+ deliveryStatus: 'failed',
63
+ deliveryError: error,
52
64
  });
53
65
  this.options.emitRun(failed);
54
66
  this.emitCurrentJob(jobId);
@@ -98,7 +98,7 @@ class SchedulerService extends node_events_1.EventEmitter {
98
98
  if (!this.isDue(job, now)) {
99
99
  continue;
100
100
  }
101
- await this.executeJob(job, now.toISOString(), false);
101
+ void this.executeJob(job, now.toISOString(), false);
102
102
  }
103
103
  }
104
104
  finally {
@@ -1,66 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createWeixinExecutionPolicy = createWeixinExecutionPolicy;
4
+ const channel_execution_policy_js_1 = require("./channel-execution-policy.js");
4
5
  function createWeixinExecutionPolicy(job, options, resolveSameThread) {
5
- if (job.executionMode === 'side-thread') {
6
- return new WeixinSideThreadExecutionPolicy(options);
7
- }
8
- return new WeixinSameThreadExecutionPolicy(options, resolveSameThread);
9
- }
10
- class WeixinSameThreadExecutionPolicy {
11
- options;
12
- resolveSameThread;
13
- constructor(options, resolveSameThread) {
14
- this.options = options;
15
- this.resolveSameThread = resolveSameThread;
16
- }
17
- async resolveTarget(job) {
18
- return {
19
- kind: 'thread',
20
- threadId: await this.resolveSameThread(job),
21
- workspaceId: job.workspaceId,
22
- platform: job.platform,
23
- route: job.route,
24
- };
25
- }
26
- beforeExecute(target) {
27
- this.options.getChannelRuntime().muteThreadBridge?.(target.threadId);
28
- }
29
- afterExecute(target) {
30
- this.options.getChannelRuntime().unmuteThreadBridge?.(target.threadId);
31
- }
32
- }
33
- class WeixinSideThreadExecutionPolicy {
34
- options;
35
- constructor(options) {
36
- this.options = options;
37
- }
38
- async resolveTarget(job) {
39
- const title = `[Scheduled] ${job.description || job.id}`;
40
- const existing = (await this.options.workspaceRouter.listThreads(job.workspaceId))
41
- .find((thread) => thread.title === title);
42
- if (existing) {
43
- return {
44
- kind: 'thread',
45
- threadId: existing.id,
46
- workspaceId: job.workspaceId,
47
- platform: job.platform,
48
- route: job.route,
49
- };
50
- }
51
- const created = await this.options.workspaceRouter.createThread(job.workspaceId, title);
52
- return {
53
- kind: 'thread',
54
- threadId: created.id,
55
- workspaceId: job.workspaceId,
56
- platform: job.platform,
57
- route: job.route,
58
- };
59
- }
60
- beforeExecute(target) {
61
- this.options.getChannelRuntime().muteThreadBridge?.(target.threadId);
62
- }
63
- afterExecute(target) {
64
- this.options.getChannelRuntime().unmuteThreadBridge?.(target.threadId);
65
- }
6
+ return (0, channel_execution_policy_js_1.createChannelExecutionPolicy)(job, options, {
7
+ platformBase: 'weixin',
8
+ resolveSameThread,
9
+ sideThreadTitle: (nextJob) => `[Scheduled:Weixin] ${nextJob.description || nextJob.id}`,
10
+ legacySideThreadTitles: (nextJob) => [`[Scheduled] ${nextJob.description || nextJob.id}`],
11
+ });
66
12
  }
@@ -12,7 +12,7 @@ class WeixinScheduleAdapter {
12
12
  this.options = options;
13
13
  this.executor = new scheduled_conversation_executor_js_1.ScheduledConversationExecutor({
14
14
  store: options.store,
15
- workspaceRouter: options.getWorkspaceRouter(),
15
+ getWorkspaceRouter: options.getWorkspaceRouter,
16
16
  });
17
17
  }
18
18
  supports(job) {
@@ -26,22 +26,13 @@ class WeixinScheduleAdapter {
26
26
  getChannelRuntime: this.options.getChannelRuntime,
27
27
  }, (nextJob) => this.resolveThread(nextJob));
28
28
  const execution = await this.executor.execute(job, job.promptTemplate, executionPolicy);
29
- let platformMessageId = '';
30
- if (execution.replyText) {
31
- const channelRuntime = this.options.getChannelRuntime();
32
- if (!channelRuntime.sendScheduledMessage) {
33
- throw new Error('WeChat channel runtime does not support scheduled delivery.');
34
- }
35
- platformMessageId = await channelRuntime.sendScheduledMessage(job.workspaceId, (0, scheduled_job_route_js_1.routeWithPlatformInstance)((0, scheduled_job_route_js_1.withoutThreadRoute)(job.route), job.platform), execution.replyText);
36
- if (!platformMessageId) {
37
- throw new Error('WeChat gateway did not return a message id for scheduled delivery.');
38
- }
39
- }
40
29
  return {
41
30
  threadId: execution.threadId,
42
31
  runId: execution.runId,
43
32
  replyText: execution.replyText,
44
- platformMessageId: platformMessageId || undefined,
33
+ deliveryMode: 'bridge-stream',
34
+ deliveryStatus: 'succeeded',
35
+ lastBridgeEventAt: new Date().toISOString(),
45
36
  };
46
37
  }
47
38
  async resolveThread(job) {
@@ -10,6 +10,7 @@ const node_path_1 = require("node:path");
10
10
  const node_os_1 = require("node:os");
11
11
  const local_core_acp_store_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-store.js");
12
12
  const local_core_acp_session_coordinator_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-session-coordinator.js");
13
+ const local_core_acp_turn_coordinator_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js");
13
14
  const scheduler_service_js_1 = require("../../services/local-ai-core/src/scheduler/scheduler-service.js");
14
15
  const lark_schedule_adapter_js_1 = require("../../services/local-ai-core/src/scheduler/lark-schedule-adapter.js");
15
16
  const bootstrap_js_1 = require("../../services/local-ai-core/src/kernel/bootstrap.js");
@@ -165,10 +166,12 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
165
166
  enabled: true,
166
167
  });
167
168
  let sentThreadId = '';
168
- let deliveredRoute;
169
+ let registeredBridge;
170
+ let unregisteredBridge = false;
169
171
  const adapter = new lark_schedule_adapter_js_1.LarkScheduleAdapter({
170
172
  store,
171
173
  getWorkspaceRouter: () => ({
174
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
172
175
  getThread: async (threadId) => ({
173
176
  id: threadId,
174
177
  workspaceId: 'workspace-a',
@@ -191,23 +194,101 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
191
194
  },
192
195
  }),
193
196
  getChannelRuntime: () => ({
194
- muteThreadBridge: () => { },
195
- unmuteThreadBridge: () => { },
196
- sendScheduledMessage: async (_workspaceId, route) => {
197
- deliveredRoute = route;
198
- return 'platform-message-1';
197
+ registerScheduledThreadBridge: (input) => {
198
+ registeredBridge = input;
199
+ return () => {
200
+ unregisteredBridge = true;
201
+ };
202
+ },
203
+ sendScheduledMessage: async () => {
204
+ throw new Error('scheduled Lark replies should be delivered through bridge events');
199
205
  },
200
206
  }),
201
207
  });
202
208
  const result = await adapter.execute({ job, triggeredAt: now });
203
209
  strict_1.default.equal(sentThreadId, latestThread.id);
204
210
  strict_1.default.equal(result.threadId, latestThread.id);
205
- strict_1.default.equal(result.platformMessageId, 'platform-message-1');
206
- strict_1.default.deepEqual(deliveredRoute, {
207
- type: 'channel.chat',
208
- channelId: 'chat-1',
209
- participantId: 'user-1',
211
+ strict_1.default.equal('platformMessageId' in result, false);
212
+ strict_1.default.deepEqual(registeredBridge, {
213
+ workspaceId: 'workspace-a',
214
+ platform: 'lark',
215
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: oldThread.id },
216
+ threadId: latestThread.id,
217
+ sessionKey: `session:${latestThread.id}`,
210
218
  });
219
+ strict_1.default.equal(unregisteredBridge, true);
220
+ store.close();
221
+ }
222
+ finally {
223
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
224
+ }
225
+ });
226
+ (0, node_test_1.default)('Lark scheduled execution resolves workspace router at execution time', async () => {
227
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-lark-lazy-router-'));
228
+ try {
229
+ const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
230
+ const thread = store.createThread('workspace-a', 'Lark thread');
231
+ const now = new Date().toISOString();
232
+ store.upsertPlatformThreadBinding({
233
+ workspace_id: 'workspace-a',
234
+ platform: 'lark:lark-1',
235
+ chat_id: 'chat-1',
236
+ platform_user_id: 'user-1',
237
+ thread_id: thread.id,
238
+ last_platform_message_id: null,
239
+ created_at: now,
240
+ updated_at: now,
241
+ });
242
+ const job = store.createScheduledJob({
243
+ workspaceId: 'workspace-a',
244
+ platform: 'lark:lark-1',
245
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
246
+ executionMode: 'same-thread',
247
+ triggerType: 'cron',
248
+ cronExpr: '30 18 * * *',
249
+ promptTemplate: 'ping',
250
+ description: 'bound lark task',
251
+ enabled: true,
252
+ });
253
+ let sentThreadId = '';
254
+ let workspaceRouter;
255
+ const adapter = new lark_schedule_adapter_js_1.LarkScheduleAdapter({
256
+ store,
257
+ getWorkspaceRouter: () => workspaceRouter,
258
+ getChannelRuntime: () => ({
259
+ registerScheduledThreadBridge: () => () => { },
260
+ sendScheduledMessage: async () => {
261
+ throw new Error('scheduled Lark replies should be delivered through bridge events');
262
+ },
263
+ }),
264
+ });
265
+ workspaceRouter = {
266
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
267
+ getThread: async (threadId) => ({
268
+ id: threadId,
269
+ workspaceId: 'workspace-a',
270
+ title: 'Thread',
271
+ live: false,
272
+ updatedAt: now,
273
+ createdAt: now,
274
+ historyCount: 1,
275
+ excerpt: '',
276
+ bridgeSessionKey: '',
277
+ agentType: 'localcore-acp',
278
+ selectedKnowledgeBaseIds: [],
279
+ pendingPermissionRequest: null,
280
+ messages: [{ id: 'message-1', role: 'assistant', kind: 'final', content: 'pong', timestamp: now }],
281
+ }),
282
+ sendThreadMessage: async (threadId) => {
283
+ sentThreadId = threadId;
284
+ store.updateRun('run-1', threadId, 'completed');
285
+ return { runId: 'run-1' };
286
+ },
287
+ };
288
+ const result = await adapter.execute({ job, triggeredAt: now });
289
+ strict_1.default.equal(sentThreadId, thread.id);
290
+ strict_1.default.equal(result.threadId, thread.id);
291
+ strict_1.default.equal('platformMessageId' in result, false);
211
292
  store.close();
212
293
  }
213
294
  finally {
@@ -241,10 +322,11 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
241
322
  description: 'bound lark task',
242
323
  enabled: true,
243
324
  });
244
- let deliveredRoute;
325
+ let registeredBridge;
245
326
  const adapter = new lark_schedule_adapter_js_1.LarkScheduleAdapter({
246
327
  store,
247
328
  getWorkspaceRouter: () => ({
329
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
248
330
  getThread: async (threadId) => ({
249
331
  id: threadId,
250
332
  workspaceId: 'workspace-a',
@@ -270,22 +352,24 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
270
352
  },
271
353
  }),
272
354
  getChannelRuntime: () => ({
273
- muteThreadBridge: () => { },
274
- unmuteThreadBridge: () => { },
275
- sendScheduledMessage: async (_workspaceId, route) => {
276
- deliveredRoute = route;
277
- return 'platform-message-1';
355
+ registerScheduledThreadBridge: (input) => {
356
+ registeredBridge = input;
357
+ return () => { };
358
+ },
359
+ sendScheduledMessage: async () => {
360
+ throw new Error('scheduled Lark replies should be delivered through bridge events');
278
361
  },
279
362
  }),
280
363
  });
281
364
  const result = await adapter.execute({ job, triggeredAt: now });
282
365
  strict_1.default.equal(result.threadId, thread.id);
283
- strict_1.default.equal(result.platformMessageId, 'platform-message-1');
284
- strict_1.default.deepEqual(deliveredRoute, {
285
- type: 'channel.chat',
286
- channelId: 'chat-1',
287
- instanceId: 'lark-1',
288
- participantId: 'user-1',
366
+ strict_1.default.equal('platformMessageId' in result, false);
367
+ strict_1.default.deepEqual(registeredBridge, {
368
+ workspaceId: 'workspace-a',
369
+ platform: 'lark:lark-1',
370
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
371
+ threadId: thread.id,
372
+ sessionKey: `session:${thread.id}`,
289
373
  });
290
374
  store.close();
291
375
  }
@@ -421,6 +505,59 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
421
505
  (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
422
506
  }
423
507
  });
508
+ (0, node_test_1.default)('ACP permission requests honor scheduled session permission override', () => {
509
+ const sentPayloads = [];
510
+ let approvalCreated = false;
511
+ let awaitingInput = false;
512
+ const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
513
+ emitBridge: () => { },
514
+ appendMessage: () => { },
515
+ updateRunStatus: () => {
516
+ awaitingInput = true;
517
+ },
518
+ createApprovalRequest: () => {
519
+ approvalCreated = true;
520
+ return 'approval-1';
521
+ },
522
+ getThreadAgentMode: () => 'default',
523
+ sendRaw: (_session, payload) => {
524
+ sentPayloads.push(payload);
525
+ return true;
526
+ },
527
+ });
528
+ coordinator.handleAgentRequest({
529
+ threadId: 'thread-1',
530
+ bridgeSessionKey: 'bridge-1',
531
+ currentRunId: 'run-1',
532
+ currentTurn: null,
533
+ pendingPermissionByRun: new Map(),
534
+ schedulerJobCreatedByRun: new Map(),
535
+ launchPermissionMode: 'bypassPermissions',
536
+ }, {
537
+ jsonrpc: '2.0',
538
+ id: 42,
539
+ method: 'session/request_permission',
540
+ params: {
541
+ toolCall: { title: 'Write AI早报/AI早报-2026-05-08.md' },
542
+ options: [
543
+ { optionId: 'deny', name: 'Deny', kind: 'deny' },
544
+ { optionId: 'allow', name: 'Allow', kind: 'allow' },
545
+ ],
546
+ },
547
+ });
548
+ strict_1.default.equal(approvalCreated, false);
549
+ strict_1.default.equal(awaitingInput, false);
550
+ strict_1.default.deepEqual(sentPayloads[0], {
551
+ jsonrpc: '2.0',
552
+ id: 42,
553
+ result: {
554
+ outcome: {
555
+ outcome: 'selected',
556
+ optionId: 'allow',
557
+ },
558
+ },
559
+ });
560
+ });
424
561
  (0, node_test_1.default)('scheduler uses short ids for new jobs and resolves legacy full ids by short id', () => {
425
562
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-id-store-'));
426
563
  try {
@@ -467,6 +604,78 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
467
604
  (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
468
605
  }
469
606
  });
607
+ (0, node_test_1.default)('scheduler dispatches due jobs without waiting for long-running jobs', async () => {
608
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-dispatch-'));
609
+ try {
610
+ const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
611
+ store.createScheduledJob({
612
+ workspaceId: 'workspace-a',
613
+ platform: 'local',
614
+ route: { type: 'local.thread', channelId: 'workspace-a', threadId: 'thread-1' },
615
+ executionMode: 'side-thread',
616
+ triggerType: 'cron',
617
+ cronExpr: '* * * * *',
618
+ promptTemplate: 'slow',
619
+ description: 'slow job',
620
+ enabled: true,
621
+ });
622
+ store.createScheduledJob({
623
+ workspaceId: 'workspace-a',
624
+ platform: 'local',
625
+ route: { type: 'local.thread', channelId: 'workspace-a', threadId: 'thread-2' },
626
+ executionMode: 'side-thread',
627
+ triggerType: 'cron',
628
+ cronExpr: '* * * * *',
629
+ promptTemplate: 'fast',
630
+ description: 'fast job',
631
+ enabled: true,
632
+ });
633
+ let releaseSlow;
634
+ const slowDone = new Promise((resolve) => {
635
+ releaseSlow = resolve;
636
+ });
637
+ const started = [];
638
+ let slowJobId = '';
639
+ const scheduler = new scheduler_service_js_1.SchedulerService({
640
+ store,
641
+ triggers: [{
642
+ triggerTypes: ['cron'],
643
+ supports: () => true,
644
+ isDue: () => true,
645
+ }],
646
+ executors: [{
647
+ deliveryTargets: ['local'],
648
+ supports: () => true,
649
+ execute: async ({ job }) => {
650
+ const isFirstStartedJob = started.length === 0;
651
+ started.push(job.id);
652
+ if (isFirstStartedJob) {
653
+ slowJobId = job.id;
654
+ await slowDone;
655
+ }
656
+ return {
657
+ threadId: String(job.route.threadId || job.id),
658
+ runId: `run:${job.id}`,
659
+ };
660
+ },
661
+ }],
662
+ eventBus: { emit: () => { }, on: () => () => { } },
663
+ });
664
+ await scheduler.tick();
665
+ await new Promise((resolve) => setImmediate(resolve));
666
+ strict_1.default.equal(started.length, 2);
667
+ const fastJobId = started.find((id) => id !== slowJobId);
668
+ strict_1.default.equal(store.listScheduledJobRuns(slowJobId)[0]?.status, 'running');
669
+ strict_1.default.equal(store.listScheduledJobRuns(fastJobId)[0]?.status, 'succeeded');
670
+ releaseSlow();
671
+ await new Promise((resolve) => setImmediate(resolve));
672
+ strict_1.default.equal(store.listScheduledJobRuns(slowJobId)[0]?.status, 'succeeded');
673
+ store.close();
674
+ }
675
+ finally {
676
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
677
+ }
678
+ });
470
679
  (0, node_test_1.default)('scheduled jobs normalize enum-like input before persistence', () => {
471
680
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-enum-store-'));
472
681
  try {
@@ -563,10 +772,21 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
563
772
  description: 'daily ping',
564
773
  enabled: true,
565
774
  });
566
- const run = store.createScheduledJobRun(job.id, 'complete');
775
+ const run = store.createScheduledJobRun(job.id, 'complete', {
776
+ deliveryMode: 'bridge-stream',
777
+ deliveryStatus: 'streaming',
778
+ platformMessageIds: ['msg-1', 'msg-2'],
779
+ lastBridgeEventAt: '2026-04-22T06:00:03.000Z',
780
+ });
567
781
  strict_1.default.equal(run.status, 'succeeded');
782
+ strict_1.default.equal(run.deliveryMode, 'bridge-stream');
783
+ strict_1.default.equal(run.deliveryStatus, 'streaming');
784
+ strict_1.default.deepEqual(run.platformMessageIds, ['msg-1', 'msg-2']);
785
+ strict_1.default.equal(run.lastBridgeEventAt, '2026-04-22T06:00:03.000Z');
568
786
  const skipped = store.updateScheduledJobRun(run.id, { status: 'cancelled' });
569
787
  strict_1.default.equal(skipped.status, 'skipped');
788
+ strict_1.default.equal(skipped.deliveryMode, 'bridge-stream');
789
+ strict_1.default.deepEqual(skipped.platformMessageIds, ['msg-1', 'msg-2']);
570
790
  strict_1.default.equal(store.getScheduledJob(job.id)?.lastStatus, 'skipped');
571
791
  store.close();
572
792
  }