opencode-orchestrator 1.2.11 → 1.2.13

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.
@@ -5,3 +5,4 @@ export * from "./types/index.js";
5
5
  export * from "./interfaces/index.js";
6
6
  export { ConcurrencyController } from "./concurrency.js";
7
7
  export { ParallelAgentManager, parallelAgentManager } from "./manager.js";
8
+ export { taskWAL } from "./persistence/task-wal.js";
@@ -15,6 +15,7 @@ export declare class TaskPoller {
15
15
  private pruneExpiredTasks;
16
16
  private onTaskComplete?;
17
17
  private pollingInterval?;
18
+ private messageCache;
18
19
  constructor(client: OpencodeClient, store: TaskStore, concurrency: ConcurrencyController, notifyParentIfAllComplete: (parentSessionID: string) => Promise<void>, scheduleCleanup: (taskId: string) => void, pruneExpiredTasks: () => void, onTaskComplete?: ((task: ParallelTask) => void | Promise<void>) | undefined);
19
20
  start(): void;
20
21
  stop(): void;
@@ -0,0 +1,11 @@
1
+ export declare class CleanupScheduler {
2
+ private intervals;
3
+ private directory;
4
+ constructor(directory: string);
5
+ start(): void;
6
+ private schedule;
7
+ stop(): void;
8
+ compactWAL(): Promise<void>;
9
+ cleanDocs(): Promise<void>;
10
+ rotateHistory(): Promise<void>;
11
+ }
@@ -13,12 +13,14 @@
13
13
  import type { PluginInput } from "@opencode-ai/plugin";
14
14
  import type { ConcurrencyController } from "../agents/concurrency.js";
15
15
  import { type TaskStatus, type TrackedTask, type TaskCompletionInfo } from "../../shared/index.js";
16
+ import type { TodoSyncService } from "../sync/todo-sync-service.js";
16
17
  export type { TaskStatus, TrackedTask, TaskCompletionInfo } from "../../shared/index.js";
17
18
  type OpencodeClient = PluginInput["client"];
18
19
  export declare class TaskToastManager {
19
20
  private tasks;
20
21
  private client;
21
22
  private concurrency;
23
+ private todoSync;
22
24
  /**
23
25
  * Initialize the manager with OpenCode client
24
26
  */
@@ -27,6 +29,10 @@ export declare class TaskToastManager {
27
29
  * Set concurrency controller (can be set after init)
28
30
  */
29
31
  setConcurrencyController(concurrency: ConcurrencyController): void;
32
+ /**
33
+ * Set TodoSyncService for TUI status synchronization
34
+ */
35
+ setTodoSync(service: TodoSyncService): void;
30
36
  /**
31
37
  * Add a new task and show consolidated toast
32
38
  */
@@ -0,0 +1,5 @@
1
+ import { type Todo } from "../../shared/index.js";
2
+ /**
3
+ * Parses markdown content into Todo objects
4
+ */
5
+ export declare function parseTodoMd(content: string): Todo[];
@@ -0,0 +1,35 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ type OpencodeClient = PluginInput["client"];
3
+ interface TrackedTaskTodo {
4
+ id: string;
5
+ description: string;
6
+ status: string;
7
+ agent: string;
8
+ isBackground: boolean;
9
+ parentSessionID?: string;
10
+ }
11
+ export declare class TodoSyncService {
12
+ private client;
13
+ private directory;
14
+ private todoPath;
15
+ private fileTodos;
16
+ private taskTodos;
17
+ private updateTimeout;
18
+ private watcher;
19
+ private activeSessions;
20
+ constructor(client: OpencodeClient, directory: string);
21
+ start(): Promise<void>;
22
+ registerSession(sessionID: string): void;
23
+ unregisterSession(sessionID: string): void;
24
+ private reloadFileTodos;
25
+ /**
26
+ * Called by TaskToastManager when tasks change
27
+ */
28
+ updateTaskStatus(task: TrackedTaskTodo): void;
29
+ removeTask(taskId: string): void;
30
+ private broadcastUpdate;
31
+ private scheduleUpdate;
32
+ private sendTodosToSession;
33
+ stop(): void;
34
+ }
35
+ export {};
package/dist/index.js CHANGED
@@ -5978,15 +5978,15 @@ function mergeDefs(...defs) {
5978
5978
  function cloneDef(schema) {
5979
5979
  return mergeDefs(schema._zod.def);
5980
5980
  }
5981
- function getElementAtPath(obj, path10) {
5982
- if (!path10)
5981
+ function getElementAtPath(obj, path12) {
5982
+ if (!path12)
5983
5983
  return obj;
5984
- return path10.reduce((acc, key) => acc?.[key], obj);
5984
+ return path12.reduce((acc, key) => acc?.[key], obj);
5985
5985
  }
5986
5986
  function promiseAllObject(promisesObj) {
5987
5987
  const keys = Object.keys(promisesObj);
5988
- const promises2 = keys.map((key) => promisesObj[key]);
5989
- return Promise.all(promises2).then((results) => {
5988
+ const promises4 = keys.map((key) => promisesObj[key]);
5989
+ return Promise.all(promises4).then((results) => {
5990
5990
  const resolvedObj = {};
5991
5991
  for (let i = 0; i < keys.length; i++) {
5992
5992
  resolvedObj[keys[i]] = results[i];
@@ -6342,11 +6342,11 @@ function aborted(x, startIndex = 0) {
6342
6342
  }
6343
6343
  return false;
6344
6344
  }
6345
- function prefixIssues(path10, issues) {
6345
+ function prefixIssues(path12, issues) {
6346
6346
  return issues.map((iss) => {
6347
6347
  var _a2;
6348
6348
  (_a2 = iss).path ?? (_a2.path = []);
6349
- iss.path.unshift(path10);
6349
+ iss.path.unshift(path12);
6350
6350
  return iss;
6351
6351
  });
6352
6352
  }
@@ -6514,7 +6514,7 @@ function treeifyError(error92, _mapper) {
6514
6514
  return issue3.message;
6515
6515
  };
6516
6516
  const result = { errors: [] };
6517
- const processError = (error93, path10 = []) => {
6517
+ const processError = (error93, path12 = []) => {
6518
6518
  var _a2, _b;
6519
6519
  for (const issue3 of error93.issues) {
6520
6520
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -6524,7 +6524,7 @@ function treeifyError(error92, _mapper) {
6524
6524
  } else if (issue3.code === "invalid_element") {
6525
6525
  processError({ issues: issue3.issues }, issue3.path);
6526
6526
  } else {
6527
- const fullpath = [...path10, ...issue3.path];
6527
+ const fullpath = [...path12, ...issue3.path];
6528
6528
  if (fullpath.length === 0) {
6529
6529
  result.errors.push(mapper(issue3));
6530
6530
  continue;
@@ -6556,8 +6556,8 @@ function treeifyError(error92, _mapper) {
6556
6556
  }
6557
6557
  function toDotPath(_path) {
6558
6558
  const segs = [];
6559
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6560
- for (const seg of path10) {
6559
+ const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6560
+ for (const seg of path12) {
6561
6561
  if (typeof seg === "number")
6562
6562
  segs.push(`[${seg}]`);
6563
6563
  else if (typeof seg === "symbol")
@@ -18829,6 +18829,7 @@ var TaskToastManager = class {
18829
18829
  tasks = /* @__PURE__ */ new Map();
18830
18830
  client = null;
18831
18831
  concurrency = null;
18832
+ todoSync = null;
18832
18833
  /**
18833
18834
  * Initialize the manager with OpenCode client
18834
18835
  */
@@ -18842,6 +18843,12 @@ var TaskToastManager = class {
18842
18843
  setConcurrencyController(concurrency) {
18843
18844
  this.concurrency = concurrency;
18844
18845
  }
18846
+ /**
18847
+ * Set TodoSyncService for TUI status synchronization
18848
+ */
18849
+ setTodoSync(service) {
18850
+ this.todoSync = service;
18851
+ }
18845
18852
  /**
18846
18853
  * Add a new task and show consolidated toast
18847
18854
  */
@@ -18857,6 +18864,7 @@ var TaskToastManager = class {
18857
18864
  sessionID: task.sessionID
18858
18865
  };
18859
18866
  this.tasks.set(task.id, trackedTask);
18867
+ this.todoSync?.updateTaskStatus(trackedTask);
18860
18868
  this.showTaskListToast(trackedTask);
18861
18869
  }
18862
18870
  /**
@@ -18866,6 +18874,7 @@ var TaskToastManager = class {
18866
18874
  const task = this.tasks.get(id);
18867
18875
  if (task) {
18868
18876
  task.status = status;
18877
+ this.todoSync?.updateTaskStatus(task);
18869
18878
  }
18870
18879
  }
18871
18880
  /**
@@ -18873,6 +18882,7 @@ var TaskToastManager = class {
18873
18882
  */
18874
18883
  removeTask(id) {
18875
18884
  this.tasks.delete(id);
18885
+ this.todoSync?.removeTask(id);
18876
18886
  }
18877
18887
  /**
18878
18888
  * Get all running tasks (newest first)
@@ -20215,15 +20225,15 @@ function mergeDefs2(...defs) {
20215
20225
  function cloneDef2(schema) {
20216
20226
  return mergeDefs2(schema._zod.def);
20217
20227
  }
20218
- function getElementAtPath2(obj, path10) {
20219
- if (!path10)
20228
+ function getElementAtPath2(obj, path12) {
20229
+ if (!path12)
20220
20230
  return obj;
20221
- return path10.reduce((acc, key) => acc?.[key], obj);
20231
+ return path12.reduce((acc, key) => acc?.[key], obj);
20222
20232
  }
20223
20233
  function promiseAllObject2(promisesObj) {
20224
20234
  const keys = Object.keys(promisesObj);
20225
- const promises2 = keys.map((key) => promisesObj[key]);
20226
- return Promise.all(promises2).then((results) => {
20235
+ const promises4 = keys.map((key) => promisesObj[key]);
20236
+ return Promise.all(promises4).then((results) => {
20227
20237
  const resolvedObj = {};
20228
20238
  for (let i = 0; i < keys.length; i++) {
20229
20239
  resolvedObj[keys[i]] = results[i];
@@ -20601,11 +20611,11 @@ function aborted2(x, startIndex = 0) {
20601
20611
  }
20602
20612
  return false;
20603
20613
  }
20604
- function prefixIssues2(path10, issues) {
20614
+ function prefixIssues2(path12, issues) {
20605
20615
  return issues.map((iss) => {
20606
20616
  var _a2;
20607
20617
  (_a2 = iss).path ?? (_a2.path = []);
20608
- iss.path.unshift(path10);
20618
+ iss.path.unshift(path12);
20609
20619
  return iss;
20610
20620
  });
20611
20621
  }
@@ -20788,7 +20798,7 @@ function formatError2(error92, mapper = (issue3) => issue3.message) {
20788
20798
  }
20789
20799
  function treeifyError2(error92, mapper = (issue3) => issue3.message) {
20790
20800
  const result = { errors: [] };
20791
- const processError = (error93, path10 = []) => {
20801
+ const processError = (error93, path12 = []) => {
20792
20802
  var _a2, _b;
20793
20803
  for (const issue3 of error93.issues) {
20794
20804
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -20798,7 +20808,7 @@ function treeifyError2(error92, mapper = (issue3) => issue3.message) {
20798
20808
  } else if (issue3.code === "invalid_element") {
20799
20809
  processError({ issues: issue3.issues }, issue3.path);
20800
20810
  } else {
20801
- const fullpath = [...path10, ...issue3.path];
20811
+ const fullpath = [...path12, ...issue3.path];
20802
20812
  if (fullpath.length === 0) {
20803
20813
  result.errors.push(mapper(issue3));
20804
20814
  continue;
@@ -20830,8 +20840,8 @@ function treeifyError2(error92, mapper = (issue3) => issue3.message) {
20830
20840
  }
20831
20841
  function toDotPath2(_path) {
20832
20842
  const segs = [];
20833
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
20834
- for (const seg of path10) {
20843
+ const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
20844
+ for (const seg of path12) {
20835
20845
  if (typeof seg === "number")
20836
20846
  segs.push(`[${seg}]`);
20837
20847
  else if (typeof seg === "symbol")
@@ -32808,13 +32818,13 @@ function resolveRef(ref, ctx) {
32808
32818
  if (!ref.startsWith("#")) {
32809
32819
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
32810
32820
  }
32811
- const path10 = ref.slice(1).split("/").filter(Boolean);
32812
- if (path10.length === 0) {
32821
+ const path12 = ref.slice(1).split("/").filter(Boolean);
32822
+ if (path12.length === 0) {
32813
32823
  return ctx.rootSchema;
32814
32824
  }
32815
32825
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
32816
- if (path10[0] === defsKey) {
32817
- const key = path10[1];
32826
+ if (path12[0] === defsKey) {
32827
+ const key = path12[1];
32818
32828
  if (!key || !ctx.defs[key]) {
32819
32829
  throw new Error(`Reference not found: ${ref}`);
32820
32830
  }
@@ -33625,6 +33635,7 @@ var TaskPoller = class {
33625
33635
  this.onTaskComplete = onTaskComplete;
33626
33636
  }
33627
33637
  pollingInterval;
33638
+ messageCache = /* @__PURE__ */ new Map();
33628
33639
  start() {
33629
33640
  if (this.pollingInterval) return;
33630
33641
  log("[task-poller.ts] start() - polling started");
@@ -33717,7 +33728,16 @@ var TaskPoller = class {
33717
33728
  }
33718
33729
  async updateTaskProgress(task) {
33719
33730
  try {
33731
+ const cached3 = this.messageCache.get(task.sessionID);
33732
+ const statusResult = await this.client.session.status();
33733
+ const sessionInfo = statusResult.data?.[task.sessionID];
33734
+ const currentMsgCount = sessionInfo?.messageCount ?? 0;
33735
+ if (cached3 && cached3.count === currentMsgCount) {
33736
+ task.stablePolls = (task.stablePolls ?? 0) + 1;
33737
+ return;
33738
+ }
33720
33739
  const result = await this.client.session.messages({ path: { id: task.sessionID } });
33740
+ this.messageCache.set(task.sessionID, { count: currentMsgCount, lastChecked: /* @__PURE__ */ new Date() });
33721
33741
  if (result.error) return;
33722
33742
  const messages = result.data ?? [];
33723
33743
  const assistantMsgs = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT);
@@ -33741,9 +33761,8 @@ var TaskPoller = class {
33741
33761
  lastMessage: lastMessage?.slice(0, 100),
33742
33762
  lastUpdate: /* @__PURE__ */ new Date()
33743
33763
  };
33744
- const currentMsgCount = messages.length;
33745
33764
  if (task.lastMsgCount === currentMsgCount) {
33746
- task.stablePolls = (task.stablePolls ?? 0) + 1;
33765
+ task.stablePolls = 0;
33747
33766
  } else {
33748
33767
  task.stablePolls = 0;
33749
33768
  }
@@ -37135,10 +37154,10 @@ async function resolveCommandPath(key, commandName) {
37135
37154
  const currentPending = pending.get(key);
37136
37155
  if (currentPending) return currentPending;
37137
37156
  const promise3 = (async () => {
37138
- const path10 = await findCommand(commandName);
37139
- cache[key] = path10;
37157
+ const path12 = await findCommand(commandName);
37158
+ cache[key] = path12;
37140
37159
  pending.delete(key);
37141
- return path10;
37160
+ return path12;
37142
37161
  })();
37143
37162
  pending.set(key, promise3);
37144
37163
  return promise3;
@@ -37147,21 +37166,21 @@ async function resolveCommandPath(key, commandName) {
37147
37166
  // src/core/notification/os-notify/notifier.ts
37148
37167
  var execAsync2 = promisify2(exec2);
37149
37168
  async function notifyDarwin(title, message) {
37150
- const path10 = await resolveCommandPath(
37169
+ const path12 = await resolveCommandPath(
37151
37170
  NOTIFICATION_COMMAND_KEYS.OSASCRIPT,
37152
37171
  NOTIFICATION_COMMANDS.OSASCRIPT
37153
37172
  );
37154
- if (!path10) return;
37173
+ if (!path12) return;
37155
37174
  const escT = title.replace(/"/g, '\\"');
37156
37175
  const escM = message.replace(/"/g, '\\"');
37157
- await execAsync2(`${path10} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"'`);
37176
+ await execAsync2(`${path12} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"'`);
37158
37177
  }
37159
37178
  async function notifyLinux(title, message) {
37160
- const path10 = await resolveCommandPath(
37179
+ const path12 = await resolveCommandPath(
37161
37180
  NOTIFICATION_COMMAND_KEYS.NOTIFY_SEND,
37162
37181
  NOTIFICATION_COMMANDS.NOTIFY_SEND
37163
37182
  );
37164
- if (path10) await execAsync2(`${path10} "${title}" "${message}" 2>/dev/null`);
37183
+ if (path12) await execAsync2(`${path12} "${title}" "${message}" 2>/dev/null`);
37165
37184
  }
37166
37185
  async function notifyWindows(title, message) {
37167
37186
  const ps = await resolveCommandPath(
@@ -37208,11 +37227,11 @@ init_os();
37208
37227
  async function playDarwin(soundPath) {
37209
37228
  if (!soundPath) return;
37210
37229
  try {
37211
- const path10 = await resolveCommandPath(
37230
+ const path12 = await resolveCommandPath(
37212
37231
  NOTIFICATION_COMMAND_KEYS.AFPLAY,
37213
37232
  NOTIFICATION_COMMANDS.AFPLAY
37214
37233
  );
37215
- if (path10) exec3(`"${path10}" "${soundPath}"`);
37234
+ if (path12) exec3(`"${path12}" "${soundPath}"`);
37216
37235
  } catch (err) {
37217
37236
  log(`[session-notify] Error playing sound (Darwin): ${err}`);
37218
37237
  }
@@ -38364,6 +38383,265 @@ var PluginManager = class _PluginManager {
38364
38383
  }
38365
38384
  };
38366
38385
 
38386
+ // src/core/sync/todo-sync-service.ts
38387
+ init_shared();
38388
+ import * as fs10 from "node:fs";
38389
+ import * as path9 from "node:path";
38390
+
38391
+ // src/core/sync/todo-parser.ts
38392
+ function parseTodoMd(content) {
38393
+ const lines = content.split("\n");
38394
+ const todos = [];
38395
+ const generateId = (text, index2) => {
38396
+ return `file-task-${index2}-${text.substring(0, 10).replace(/[^a-zA-Z0-9]/g, "")}`;
38397
+ };
38398
+ let index = 0;
38399
+ for (const line of lines) {
38400
+ const match = line.match(/^\s*-\s*\[([ xX\/\-\.])\]\s*(.+)$/);
38401
+ if (match) {
38402
+ const [, statusChar, text] = match;
38403
+ const content2 = text.trim();
38404
+ let status = "pending";
38405
+ switch (statusChar.toLowerCase()) {
38406
+ case "x":
38407
+ status = "completed";
38408
+ break;
38409
+ case "/":
38410
+ case ".":
38411
+ status = "in_progress";
38412
+ break;
38413
+ case "-":
38414
+ status = "cancelled";
38415
+ break;
38416
+ case " ":
38417
+ default:
38418
+ status = "pending";
38419
+ break;
38420
+ }
38421
+ todos.push({
38422
+ id: generateId(content2, index),
38423
+ content: content2,
38424
+ status,
38425
+ priority: "medium",
38426
+ // Default priority for file items
38427
+ createdAt: /* @__PURE__ */ new Date()
38428
+ });
38429
+ index++;
38430
+ }
38431
+ }
38432
+ return todos;
38433
+ }
38434
+
38435
+ // src/core/sync/todo-sync-service.ts
38436
+ var TodoSyncService = class {
38437
+ client;
38438
+ directory;
38439
+ todoPath;
38440
+ fileTodos = [];
38441
+ taskTodos = /* @__PURE__ */ new Map();
38442
+ updateTimeout = null;
38443
+ watcher = null;
38444
+ // We only want to sync to the "primary" session or all sessions?
38445
+ // The design says `syncTaskStore(sessionID)`.
38446
+ // Usually TUI TODO is per session.
38447
+ // However, `todo.md` is global (project level).
38448
+ // So we should probably broadcast to active sessions or just the one associated with the tasks?
38449
+ // Current TUI limitation: we might need to know which session to update.
38450
+ // For TUI sidebar, we usually update the session the user is looking at.
38451
+ // But we don't know that.
38452
+ // We will maintain a set of "active sessions" provided by index.ts or just update relevant ones.
38453
+ // For Phase 1, we might just update the sessions we know about (parents of tasks) or register sessions.
38454
+ activeSessions = /* @__PURE__ */ new Set();
38455
+ constructor(client, directory) {
38456
+ this.client = client;
38457
+ this.directory = directory;
38458
+ this.todoPath = path9.join(this.directory, PATHS.TODO);
38459
+ }
38460
+ async start() {
38461
+ await this.reloadFileTodos();
38462
+ if (fs10.existsSync(this.todoPath)) {
38463
+ let timer;
38464
+ this.watcher = fs10.watch(this.todoPath, (eventType) => {
38465
+ if (eventType === "change" || eventType === "rename") {
38466
+ clearTimeout(timer);
38467
+ timer = setTimeout(() => {
38468
+ this.reloadFileTodos().catch((err) => log(`[TodoSync] Error reloading: ${err}`));
38469
+ }, 500);
38470
+ }
38471
+ });
38472
+ }
38473
+ }
38474
+ registerSession(sessionID) {
38475
+ this.activeSessions.add(sessionID);
38476
+ this.scheduleUpdate(sessionID);
38477
+ }
38478
+ unregisterSession(sessionID) {
38479
+ this.activeSessions.delete(sessionID);
38480
+ }
38481
+ async reloadFileTodos() {
38482
+ try {
38483
+ if (fs10.existsSync(this.todoPath)) {
38484
+ const content = await fs10.promises.readFile(this.todoPath, "utf-8");
38485
+ this.fileTodos = parseTodoMd(content);
38486
+ this.broadcastUpdate();
38487
+ }
38488
+ } catch (error92) {
38489
+ log(`[TodoSync] Failed to read todo.md: ${error92}`);
38490
+ }
38491
+ }
38492
+ /**
38493
+ * Called by TaskToastManager when tasks change
38494
+ */
38495
+ updateTaskStatus(task) {
38496
+ this.taskTodos.set(task.id, task);
38497
+ if (task.parentSessionID) {
38498
+ this.scheduleUpdate(task.parentSessionID);
38499
+ } else {
38500
+ this.broadcastUpdate();
38501
+ }
38502
+ }
38503
+ removeTask(taskId) {
38504
+ const task = this.taskTodos.get(taskId);
38505
+ if (task) {
38506
+ this.taskTodos.delete(taskId);
38507
+ if (task.parentSessionID) {
38508
+ this.scheduleUpdate(task.parentSessionID);
38509
+ } else {
38510
+ this.broadcastUpdate();
38511
+ }
38512
+ }
38513
+ }
38514
+ broadcastUpdate() {
38515
+ for (const sessionID of this.activeSessions) {
38516
+ this.scheduleUpdate(sessionID);
38517
+ }
38518
+ }
38519
+ scheduleUpdate(sessionID) {
38520
+ this.sendTodosToSession(sessionID).catch((err) => {
38521
+ });
38522
+ }
38523
+ async sendTodosToSession(sessionID) {
38524
+ const taskTodosList = Array.from(this.taskTodos.values()).map((t) => {
38525
+ let status = "pending";
38526
+ const s = t.status.toLowerCase();
38527
+ if (s.includes("run") || s.includes("wait") || s.includes("que")) status = "in_progress";
38528
+ else if (s.includes("complete") || s.includes("done")) status = "completed";
38529
+ else if (s.includes("fail") || s.includes("error")) status = "cancelled";
38530
+ else if (s.includes("cancel")) status = "cancelled";
38531
+ return {
38532
+ id: `task-${t.id}`,
38533
+ // Prefix to avoid collision
38534
+ content: `[${t.agent.toUpperCase()}] ${t.description}`,
38535
+ status,
38536
+ priority: t.isBackground ? "low" : "high",
38537
+ createdAt: /* @__PURE__ */ new Date()
38538
+ };
38539
+ });
38540
+ const merged = [
38541
+ ...this.fileTodos,
38542
+ ...taskTodosList
38543
+ ];
38544
+ try {
38545
+ await this.client.session.todo({
38546
+ path: { id: sessionID },
38547
+ // Standardize to id
38548
+ body: { todos: merged }
38549
+ });
38550
+ } catch (error92) {
38551
+ }
38552
+ }
38553
+ stop() {
38554
+ if (this.watcher) {
38555
+ this.watcher.close();
38556
+ }
38557
+ }
38558
+ };
38559
+
38560
+ // src/core/cleanup/cleanup-scheduler.ts
38561
+ import * as fs11 from "node:fs";
38562
+ import * as path10 from "node:path";
38563
+ init_shared();
38564
+ var CleanupScheduler = class {
38565
+ intervals = /* @__PURE__ */ new Map();
38566
+ directory;
38567
+ constructor(directory) {
38568
+ this.directory = directory;
38569
+ }
38570
+ start() {
38571
+ this.schedule("wal-compact", () => this.compactWAL(), 10 * 60 * 1e3);
38572
+ this.schedule("docs-clean", () => this.cleanDocs(), 60 * 60 * 1e3);
38573
+ this.schedule("history-rotate", () => this.rotateHistory(), 24 * 60 * 60 * 1e3);
38574
+ log(`[Cleanup] Scheduler started`);
38575
+ }
38576
+ schedule(name, fn, intervalMs) {
38577
+ const timer = setInterval(() => {
38578
+ fn().catch((err) => log(`[Cleanup] ${name} failed:`, err));
38579
+ }, intervalMs);
38580
+ if (timer.unref) timer.unref();
38581
+ this.intervals.set(name, timer);
38582
+ }
38583
+ stop() {
38584
+ for (const timer of this.intervals.values()) {
38585
+ clearInterval(timer);
38586
+ }
38587
+ this.intervals.clear();
38588
+ log(`[Cleanup] Scheduler stopped`);
38589
+ }
38590
+ async compactWAL() {
38591
+ try {
38592
+ const manager = parallelAgentManager.getInstance();
38593
+ const activeTasks = manager.getAllTasks().filter((t) => t.status === TASK_STATUS.RUNNING || t.status === TASK_STATUS.PENDING);
38594
+ await taskWAL.compact(activeTasks);
38595
+ } catch (error92) {
38596
+ log(`[Cleanup] Failed to compact WAL: ${error92}`);
38597
+ }
38598
+ }
38599
+ async cleanDocs() {
38600
+ try {
38601
+ const stats2 = await stats();
38602
+ if (stats2.totalSize > 10 * 1024 * 1024) {
38603
+ const allDocs = await list();
38604
+ allDocs.sort((a, b) => new Date(a.fetchedAt).getTime() - new Date(b.fetchedAt).getTime());
38605
+ const toDelete = allDocs.slice(0, Math.floor(allDocs.length / 2));
38606
+ for (const doc of toDelete) {
38607
+ await remove(doc.url);
38608
+ }
38609
+ log(`[Cleanup] Pruned ${toDelete.length} documents due to size limit`);
38610
+ }
38611
+ } catch (error92) {
38612
+ }
38613
+ }
38614
+ async rotateHistory() {
38615
+ try {
38616
+ const historyPath = path10.join(this.directory, ".opencode/archive/todo_history.jsonl");
38617
+ if (!fs11.existsSync(historyPath)) return;
38618
+ const stat = await fs11.promises.stat(historyPath);
38619
+ if (stat.size === 0) return;
38620
+ const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
38621
+ const archivePath = path10.join(
38622
+ this.directory,
38623
+ `.opencode/archive/todo_history.${dateStr}.jsonl`
38624
+ );
38625
+ await fs11.promises.rename(historyPath, archivePath);
38626
+ await fs11.promises.writeFile(historyPath, "");
38627
+ const archiveDir = path10.dirname(historyPath);
38628
+ const files = await fs11.promises.readdir(archiveDir);
38629
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1e3;
38630
+ for (const file3 of files) {
38631
+ if (file3.startsWith("todo_history.") && file3.endsWith(".jsonl")) {
38632
+ const filePath = path10.join(archiveDir, file3);
38633
+ const fStat = await fs11.promises.stat(filePath);
38634
+ if (fStat.mtimeMs < cutoff) {
38635
+ await fs11.promises.unlink(filePath);
38636
+ }
38637
+ }
38638
+ }
38639
+ } catch (error92) {
38640
+ log(`[Cleanup] History rotation error: ${error92}`);
38641
+ }
38642
+ }
38643
+ };
38644
+
38367
38645
  // src/plugin-handlers/tool-execute-pre-handler.ts
38368
38646
  function createToolExecuteBeforeHandler(ctx) {
38369
38647
  const { sessions, directory } = ctx;
@@ -38427,17 +38705,17 @@ function createChatMessageHandler(ctx) {
38427
38705
  init_shared();
38428
38706
 
38429
38707
  // src/utils/compatibility/claude.ts
38430
- import fs10 from "fs";
38431
- import path9 from "path";
38708
+ import fs12 from "fs";
38709
+ import path11 from "path";
38432
38710
  function findClaudeRules(startDir = process.cwd()) {
38433
38711
  try {
38434
38712
  let currentDir = startDir;
38435
- const root = path9.parse(startDir).root;
38713
+ const root = path11.parse(startDir).root;
38436
38714
  while (true) {
38437
- const claudeMdPath = path9.join(currentDir, "CLAUDE.md");
38438
- if (fs10.existsSync(claudeMdPath)) {
38715
+ const claudeMdPath = path11.join(currentDir, "CLAUDE.md");
38716
+ if (fs12.existsSync(claudeMdPath)) {
38439
38717
  try {
38440
- const content = fs10.readFileSync(claudeMdPath, "utf-8");
38718
+ const content = fs12.readFileSync(claudeMdPath, "utf-8");
38441
38719
  log(`[compatibility] Loaded CLAUDE.md from ${claudeMdPath}`);
38442
38720
  return formatRules("CLAUDE.md", content);
38443
38721
  } catch (e) {
@@ -38445,11 +38723,11 @@ function findClaudeRules(startDir = process.cwd()) {
38445
38723
  }
38446
38724
  }
38447
38725
  if (currentDir === root) break;
38448
- currentDir = path9.dirname(currentDir);
38726
+ currentDir = path11.dirname(currentDir);
38449
38727
  }
38450
- const copilotPath = path9.join(startDir, ".github", "copilot-instructions.md");
38451
- if (fs10.existsSync(copilotPath)) {
38452
- return formatRules("Copilot Instructions", fs10.readFileSync(copilotPath, "utf-8"));
38728
+ const copilotPath = path11.join(startDir, ".github", "copilot-instructions.md");
38729
+ if (fs12.existsSync(copilotPath)) {
38730
+ return formatRules("Copilot Instructions", fs12.readFileSync(copilotPath, "utf-8"));
38453
38731
  }
38454
38732
  return null;
38455
38733
  } catch (error92) {
@@ -39054,6 +39332,11 @@ var OrchestratorPlugin = async (input) => {
39054
39332
  await pluginManager.initialize(directory);
39055
39333
  const dynamicTools = pluginManager.getDynamicTools();
39056
39334
  taskToastManager.setConcurrencyController(parallelAgentManager2.getConcurrency());
39335
+ const todoSync = new TodoSyncService(client, directory);
39336
+ await todoSync.start();
39337
+ taskToastManager.setTodoSync(todoSync);
39338
+ const cleanupScheduler = new CleanupScheduler(directory);
39339
+ cleanupScheduler.start();
39057
39340
  const handlerContext = {
39058
39341
  client,
39059
39342
  directory,
@@ -39110,7 +39393,20 @@ var OrchestratorPlugin = async (input) => {
39110
39393
  // -----------------------------------------------------------------
39111
39394
  // Event hook - handles OpenCode events
39112
39395
  // -----------------------------------------------------------------
39113
- event: createEventHandler(handlerContext),
39396
+ // -----------------------------------------------------------------
39397
+ // Event hook - handles OpenCode events
39398
+ // -----------------------------------------------------------------
39399
+ event: async (payload) => {
39400
+ const result = await createEventHandler(handlerContext)(payload);
39401
+ const { event } = payload;
39402
+ if (event.type === "session.created" && event.properties) {
39403
+ const sessionID = event.properties.sessionID || event.properties.id || event.properties.info?.sessionID;
39404
+ if (sessionID) {
39405
+ todoSync.registerSession(sessionID);
39406
+ }
39407
+ }
39408
+ return result;
39409
+ },
39114
39410
  // -----------------------------------------------------------------
39115
39411
  // chat.message hook - intercepts commands and sets up sessions
39116
39412
  // -----------------------------------------------------------------
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "opencode-orchestrator",
3
3
  "displayName": "OpenCode Orchestrator",
4
4
  "description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
5
- "version": "1.2.11",
5
+ "version": "1.2.13",
6
6
  "author": "agnusdei1207",
7
7
  "license": "MIT",
8
8
  "repository": {