@songsid/agend 2.3.0-beta.19 → 2.3.0-beta.20

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 (81) hide show
  1. package/dist/agent-cli-instructions.md +4 -0
  2. package/dist/agent-cli.js +25 -0
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/channel/mcp-tools.js +2 -1
  5. package/dist/channel/mcp-tools.js.map +1 -1
  6. package/dist/config-validator.js +19 -1
  7. package/dist/config-validator.js.map +1 -1
  8. package/dist/cross-instance-notice.d.ts +7 -0
  9. package/dist/cross-instance-notice.js +10 -0
  10. package/dist/cross-instance-notice.js.map +1 -1
  11. package/dist/daemon.js +4 -0
  12. package/dist/daemon.js.map +1 -1
  13. package/dist/delivery-outbox.js +5 -0
  14. package/dist/delivery-outbox.js.map +1 -1
  15. package/dist/fleet-manager.d.ts +26 -0
  16. package/dist/fleet-manager.js +115 -14
  17. package/dist/fleet-manager.js.map +1 -1
  18. package/dist/general-knowledge/skills/scheduling/SKILL.md +4 -0
  19. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +25 -1
  20. package/dist/instance-config-impact.js +4 -0
  21. package/dist/instance-config-impact.js.map +1 -1
  22. package/dist/log-steps.d.ts +1 -0
  23. package/dist/log-steps.js +15 -0
  24. package/dist/log-steps.js.map +1 -1
  25. package/dist/outbound-handlers.d.ts +3 -0
  26. package/dist/outbound-handlers.js +20 -4
  27. package/dist/outbound-handlers.js.map +1 -1
  28. package/dist/outbound-schemas.d.ts +33 -0
  29. package/dist/outbound-schemas.js +25 -8
  30. package/dist/outbound-schemas.js.map +1 -1
  31. package/dist/public-web-gateway.js +5 -2
  32. package/dist/public-web-gateway.js.map +1 -1
  33. package/dist/scheduler/db.d.ts +40 -13
  34. package/dist/scheduler/db.js +123 -89
  35. package/dist/scheduler/db.js.map +1 -1
  36. package/dist/scheduler/task-board-text.d.ts +26 -0
  37. package/dist/scheduler/task-board-text.js +135 -0
  38. package/dist/scheduler/task-board-text.js.map +1 -0
  39. package/dist/scheduler/task-board.d.ts +650 -0
  40. package/dist/scheduler/task-board.js +1938 -0
  41. package/dist/scheduler/task-board.js.map +1 -0
  42. package/dist/scheduler/types.d.ts +6 -1
  43. package/dist/search/search-docs.d.ts +58 -0
  44. package/dist/search/search-docs.js +63 -0
  45. package/dist/search/search-docs.js.map +1 -0
  46. package/dist/search/search-index.d.ts +141 -0
  47. package/dist/search/search-index.js +440 -0
  48. package/dist/search/search-index.js.map +1 -0
  49. package/dist/search/search-store.d.ts +122 -0
  50. package/dist/search/search-store.js +236 -0
  51. package/dist/search/search-store.js.map +1 -0
  52. package/dist/search/search-worker.d.ts +1 -0
  53. package/dist/search/search-worker.js +163 -0
  54. package/dist/search/search-worker.js.map +1 -0
  55. package/dist/task-board-tool.d.ts +25 -0
  56. package/dist/task-board-tool.js +154 -0
  57. package/dist/task-board-tool.js.map +1 -0
  58. package/dist/task-board-waker.d.ts +44 -0
  59. package/dist/task-board-waker.js +128 -0
  60. package/dist/task-board-waker.js.map +1 -0
  61. package/dist/types.d.ts +15 -0
  62. package/dist/ui/fleet-task-card.js +436 -0
  63. package/dist/ui/fleet-task-common.js +448 -0
  64. package/dist/ui/fleet-tasks.js +535 -0
  65. package/dist/ui/panel-chat.js +8 -0
  66. package/dist/ui/panel-fleet.js +21 -65
  67. package/dist/ui/search-dialog.js +349 -0
  68. package/dist/ui/shared/app-i18n.js +3 -0
  69. package/dist/ui/shared/app-shell.js +43 -1
  70. package/dist/ui/shared/app-stream.js +2 -1
  71. package/dist/ui/shared/app.css +270 -2
  72. package/dist/ui/shared/app.js +3 -1
  73. package/dist/ui/shared/ui-toast.js +16 -3
  74. package/dist/web-api.d.ts +14 -4
  75. package/dist/web-api.js +408 -10
  76. package/dist/web-api.js.map +1 -1
  77. package/dist/web-chat-history.d.ts +2 -0
  78. package/dist/web-chat-history.js +2 -0
  79. package/dist/web-chat-history.js.map +1 -1
  80. package/dist/workflow-templates/default.md +3 -0
  81. package/package.json +1 -1
@@ -73,6 +73,8 @@ import { createLogger, rotateLogIfNeeded } from "./logger.js";
73
73
  import { processAttachments } from "./channel/attachment-handler.js";
74
74
  import { routeToolCall } from "./channel/tool-router.js";
75
75
  import { Scheduler } from "./scheduler/index.js";
76
+ import { boardActivity, boardCardLimit, boardToolAction } from "./task-board-tool.js";
77
+ import { TaskBoardWaker } from "./task-board-waker.js";
76
78
  import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
77
79
  import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
78
80
  import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, parseCompactCommand, SAVE_FILENAME_RE, resolveInstanceContext, forgetInstanceContext, readStatuslineJson, statuslineModel } from "./topic-commands.js";
@@ -103,7 +105,8 @@ export const TASK_LIST_CAP = 100;
103
105
  * #1336: the non-terminal statuses a bare `list` returns by default. `done`
104
106
  * and `cancelled` are excluded unless the caller passes `filter_status`.
105
107
  */
106
- export const LIVE_TASK_STATUSES = ["open", "claimed", "blocked"];
108
+ /** Not finished: `review` is done work waiting for the person (the task board), still live. */
109
+ export const LIVE_TASK_STATUSES = ["open", "claimed", "blocked", "review"];
107
110
  /**
108
111
  * #1336 P2: shared normalization for filter_status across both task handlers
109
112
  * and the cap helper. Trims whitespace before dropping empties, so
@@ -181,7 +184,9 @@ import { runOnLoginShell } from "./login-shell.js";
181
184
  import { tightenInstanceDirs } from "./private-dir.js";
182
185
  import { decideSlash } from "./slash-authz.js";
183
186
  import { canonicalCommand, commandSpec, decideCommand, ruleFor } from "./command-table.js";
184
- import { crossInstanceVisibility, runVisibilityCommand } from "./cross-instance-notice.js";
187
+ import { crossInstanceVisibility, runVisibilityCommand, xmsgVisibility } from "./cross-instance-notice.js";
188
+ import { SEARCH_RETENTION_DAYS, SearchIndex } from "./search/search-index.js";
189
+ import { chatDoc, taskDoc, xmsgDoc } from "./search/search-docs.js";
185
190
  import { WebSettingsTools } from "./web-settings-tools.js";
186
191
  import { bindGuestRequest, guestRouteDecision } from "./web-guest-access.js";
187
192
  import { GUEST_ROOM_TICK_MS, GuestRoomMembership, guestGroupRooms } from "./web-guest-rooms.js";
@@ -666,6 +671,8 @@ export class FleetManager {
666
671
  statuslineWatcher;
667
672
  dailySummary = null;
668
673
  dailyTipScheduler = null;
674
+ /** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
675
+ taskBoardWaker = null;
669
676
  webhookEmitter = null;
670
677
  // Topic icon + auto-archive state
671
678
  topicIcons = {};
@@ -904,6 +911,8 @@ export class FleetManager {
904
911
  webChatHistory = new WebChatHistory();
905
912
  /** #1565: the web chat history's files (set at fleet start, once the home and the instances are known). */
906
913
  webChatStore = null;
914
+ /** #2293: the dashboard's global search (null: web.search.enabled is false, or the fleet has not started). */
915
+ searchIndex = null;
907
916
  /** Uploaded files and the files the dashboard may fetch back (uploads, reply attachments). */
908
917
  // #1937: the upload limits are the config's, read on each use (agend reload applies a change to the next upload).
909
918
  webFiles = new WebFileLedger({ limits: () => uploadLimits(this.fleetConfig?.web) });
@@ -1787,7 +1796,10 @@ export class FleetManager {
1787
1796
  : t(`delivery.fleet_${row.state}`, row.targetInstance, row.operationId, row.deliveryId);
1788
1797
  this.logger.error({ deliveryId: row.deliveryId, operationId: row.operationId, target: row.targetInstance, state: row.state,
1789
1798
  cause: row.lastError, ...(silent ? { silentSchedule: true } : {}) }, notice);
1790
- this.notifyFleetError(notice);
1799
+ // A self-scheduled silent heartbeat stays log-only, including the
1800
+ // operator alert; another source still receives the normal notice.
1801
+ if (!silent || row.sourceInstance !== row.targetInstance)
1802
+ this.notifyFleetError(notice);
1791
1803
  }
1792
1804
  }
1793
1805
  });
@@ -7037,6 +7049,7 @@ export class FleetManager {
7037
7049
  // #1565: the web chat as it was before this process — the configured instances' only (fleet.yaml and ClassicBot
7038
7050
  // rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
7039
7051
  this.restoreWebChat(fleet);
7052
+ this.startSearchIndex(fleet);
7040
7053
  // Poll classicBot.yaml for external changes every 30s
7041
7054
  this.classicReloadTimer = setInterval(() => {
7042
7055
  // Attributes the synchronous part, up to the reload's first await (#1235).
@@ -7181,7 +7194,9 @@ export class FleetManager {
7181
7194
  };
7182
7195
  this.scheduler = new Scheduler(join(this.dataDir, "scheduler.db"), (schedule, runId, retry) => this.handleScheduleTrigger(schedule, runId, retry), schedulerConfig, (name) => this.fleetConfig?.instances?.[name] != null || !!this.classicChannels?.getAll().some(ch => ch.instanceName === name), (schedule, retry, reason) => this.scheduleRetryDropped(schedule, retry, reason), this.logger);
7183
7196
  this.scheduler.init();
7197
+ this.scheduler.db.onTaskChange = (task) => this.searchIndex?.record(taskDoc(task));
7184
7198
  this.logger.info("Scheduler initialized");
7199
+ this.attachTaskBoard(this.scheduler.db);
7185
7200
  // Tips share the daily-report clock but remain an independent internal
7186
7201
  // job: disabling daily_summary must not disable tips (and vice versa).
7187
7202
  // The callback checks defaults.tips at fire time, so /tips on|off is hot.
@@ -10650,14 +10665,20 @@ export class FleetManager {
10650
10665
  return (v === "low" || v === "normal" || v === "high" || v === "urgent") ? v : undefined;
10651
10666
  };
10652
10667
  const asStatus = (v) => {
10653
- return (v === "open" || v === "claimed" || v === "done" || v === "blocked" || v === "cancelled") ? v : undefined;
10668
+ return (v === "open" || v === "claimed" || v === "review" || v === "done" || v === "blocked" || v === "cancelled") ? v : undefined;
10654
10669
  };
10655
10670
  // #1336 P2: use the shared normalizer — trims whitespace AND drops empties,
10656
10671
  // so " \t " / [" ","\t"] are treated identically to undefined.
10657
10672
  const asStatusFilter = normalizeStatusFilter;
10658
10673
  // #1336: small write ack — identifying fields only.
10659
10674
  const ack = (t) => ({ id: t.id, status: t.status, updated_at: t.updated_at });
10675
+ // The board's notes for the write (a send-back reason, a comment the assignee had not seen): with the ack when any.
10676
+ const noted = (a) => { const notes = db.board.takeNotes(); return notes.length ? { ...a, notes } : a; };
10660
10677
  try {
10678
+ // The task board's own actions and fields (task-board-tool.ts); the rest is the tool's plain task, below.
10679
+ const onBoard = boardToolAction(db, instance, args);
10680
+ if (onBoard !== undefined)
10681
+ return onBoard;
10661
10682
  switch (action) {
10662
10683
  case "create": {
10663
10684
  const title = asStr(args.title);
@@ -10696,24 +10717,24 @@ export class FleetManager {
10696
10717
  const id = asStr(args.id);
10697
10718
  if (!id)
10698
10719
  return { error: "id is required" };
10699
- return ack(db.claimTask(db.getTaskByPrefix(id).id, instance));
10720
+ return noted(ack(db.claimTask(db.getTaskByPrefix(id).id, instance, { kind: "instance", name: instance })));
10700
10721
  }
10701
10722
  case "done": {
10702
10723
  const id = asStr(args.id);
10703
10724
  if (!id)
10704
10725
  return { error: "id is required" };
10705
- return ack(db.completeTask(db.getTaskByPrefix(id).id, asStr(args.result)));
10726
+ return noted(ack(db.completeTask(db.getTaskByPrefix(id).id, asStr(args.result), { kind: "instance", name: instance })));
10706
10727
  }
10707
10728
  case "update": {
10708
10729
  const id = asStr(args.id);
10709
10730
  if (!id)
10710
10731
  return { error: "id is required" };
10711
- return ack(db.updateTask(db.getTaskByPrefix(id).id, {
10732
+ return noted(ack(db.updateTask(db.getTaskByPrefix(id).id, {
10712
10733
  status: asStatus(args.status),
10713
10734
  assignee: asStr(args.assignee),
10714
10735
  result: asStr(args.result),
10715
10736
  priority: asPriority(args.priority),
10716
- }));
10737
+ }, { kind: "instance", name: instance })));
10717
10738
  }
10718
10739
  default: return { error: `Unknown task action: ${action}` };
10719
10740
  }
@@ -10807,7 +10828,18 @@ export class FleetManager {
10807
10828
  // so " \t " / [" ","\t"] are treated identically to undefined.
10808
10829
  const asStatusFilter = normalizeStatusFilter;
10809
10830
  const ack = (t) => ({ id: t.id, status: t.status, updated_at: t.updated_at });
10831
+ const noted = (a) => { const notes = db.board.takeNotes(); return notes.length ? { ...a, notes } : a; };
10810
10832
  try {
10833
+ // The task board's own actions and fields (task-board-tool.ts); the rest is the tool's plain task, below.
10834
+ const onBoard = boardToolAction(db, meta.instance_name || instanceName, payload);
10835
+ if (onBoard !== undefined) {
10836
+ ipc.send({ type: "fleet_task_response", fleetRequestId, result: onBoard });
10837
+ // The activity log hears of board work as it does of plain tasks.
10838
+ const what = boardActivity(payload);
10839
+ if (what)
10840
+ this.eventLog?.logActivity("task_update", instanceName, what);
10841
+ return;
10842
+ }
10811
10843
  let result;
10812
10844
  // Full task kept for activity logging; the IPC reply uses the compact ack.
10813
10845
  let logTask;
@@ -10843,12 +10875,12 @@ export class FleetManager {
10843
10875
  result = db.getTaskByPrefix(payload.id);
10844
10876
  break;
10845
10877
  case "claim":
10846
- logTask = db.claimTask(db.getTaskByPrefix(payload.id).id, meta.instance_name || instanceName);
10847
- result = ack(logTask);
10878
+ logTask = db.claimTask(db.getTaskByPrefix(payload.id).id, meta.instance_name || instanceName, { kind: "instance", name: meta.instance_name || instanceName });
10879
+ result = noted(ack(logTask));
10848
10880
  break;
10849
10881
  case "done":
10850
- logTask = db.completeTask(db.getTaskByPrefix(payload.id).id, payload.result);
10851
- result = ack(logTask);
10882
+ logTask = db.completeTask(db.getTaskByPrefix(payload.id).id, payload.result, { kind: "instance", name: meta.instance_name || instanceName });
10883
+ result = noted(ack(logTask));
10852
10884
  break;
10853
10885
  case "update":
10854
10886
  logTask = db.updateTask(db.getTaskByPrefix(payload.id).id, {
@@ -10856,8 +10888,8 @@ export class FleetManager {
10856
10888
  assignee: payload.assignee,
10857
10889
  result: payload.result,
10858
10890
  priority: payload.priority,
10859
- });
10860
- result = ack(logTask);
10891
+ }, { kind: "instance", name: meta.instance_name || instanceName });
10892
+ result = noted(ack(logTask));
10861
10893
  break;
10862
10894
  default:
10863
10895
  throw new Error(`Unknown task action: ${action}`);
@@ -11480,6 +11512,7 @@ export class FleetManager {
11480
11512
  // Its web chat goes with it (only after the removal succeeded): a later instance of the same name
11481
11513
  // must not be shown the old one's conversation, and deleted names must not pile up.
11482
11514
  this.webChatHistory.forget(name);
11515
+ this.searchIndex?.removeInstance(name);
11483
11516
  this.webFiles.forget(name);
11484
11517
  this.compactWatchService?.forget(name);
11485
11518
  // #1664: and it is nobody's favorite any more (every device sees that at its next status frame).
@@ -16110,6 +16143,36 @@ export class FleetManager {
16110
16143
  const failure = await this.promptLoginBackends(chat);
16111
16144
  await data.respond(failure ?? t("login.chooser_posted"));
16112
16145
  }
16146
+ /** #2293: whose cross-instance messages search leaves out now (web-api /ui/search). */
16147
+ searchVisibility() { return xmsgVisibility(this.fleetConfig); }
16148
+ /** #2293: a cross-instance message for the search index (outbound-handlers, after it was admitted). */
16149
+ indexCrossInstance(record) {
16150
+ this.searchIndex?.record(xmsgDoc(record));
16151
+ }
16152
+ /**
16153
+ * #2293: the global search index, at fleet start — after the web chat is restored (the backfill reads it). Its worker
16154
+ * owns search.db; see search-index.ts. Off with web.search.enabled: false.
16155
+ */
16156
+ startSearchIndex(fleet) {
16157
+ const config = fleet.web?.search;
16158
+ if (config?.enabled === false || this.searchIndex)
16159
+ return;
16160
+ const history = this.webChatHistory;
16161
+ this.searchIndex = new SearchIndex({
16162
+ path: join(this.dataDir, "search.db"),
16163
+ retentionDays: config?.retention_days ?? SEARCH_RETENTION_DAYS,
16164
+ outboxPath: join(this.dataDir, "delivery-outbox.db"),
16165
+ schedulerPath: join(this.dataDir, "scheduler.db"),
16166
+ chatDocs: function* () { for (const name of history.instances())
16167
+ for (const m of history.list(name)) {
16168
+ const doc = chatDoc(m);
16169
+ if (doc)
16170
+ yield doc;
16171
+ } },
16172
+ log: (level, msg) => this.logger[level](msg),
16173
+ });
16174
+ this.searchIndex.start();
16175
+ }
16113
16176
  queueMirrorMessage(text) {
16114
16177
  const mirrorTopicId = this.fleetConfig?.channel?.mirror_topic_id;
16115
16178
  if (mirrorTopicId == null || !this.adapter)
@@ -16207,6 +16270,7 @@ export class FleetManager {
16207
16270
  replyTo: m.replyTo,
16208
16271
  });
16209
16272
  broadcastSseEvent(this.sseClients, event, recorded, onError, this.webChatHistory.cursorOf(recorded));
16273
+ this.searchIndex?.record(chatDoc(recorded));
16210
16274
  // #1629: live-only receipt after the recorded agent reply. Never persist/replay it or give it an SSE cursor.
16211
16275
  if (recorded.role === "agent")
16212
16276
  broadcastSseEvent(this.sseClients, "agent_reply_live", { v: 1, boot: recorded.boot, id: recorded.id, instance: recorded.instance }, onError);
@@ -16219,6 +16283,38 @@ export class FleetManager {
16219
16283
  }
16220
16284
  broadcastSseEvent(this.sseClients, event, data, onError);
16221
16285
  }
16286
+ /**
16287
+ * The task board's live side: every committed change goes to the open pages (never to a guest) — the board's new
16288
+ * revision and the card numbers it touched; not who touched them (the page does not need it, and a public-link page
16289
+ * must not learn a hidden instance's name from it). Its news goes to the instances whose cards someone else changed:
16290
+ * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken
16291
+ * (task-board-waker.ts).
16292
+ */
16293
+ attachTaskBoard(db) {
16294
+ this.taskBoardWaker?.stop();
16295
+ // #2303: each instance's cards at once, read at every claim (a fleet.yaml edit applies to the next one).
16296
+ db.board.setCardLimit((name) => boardCardLimit(this.fleetConfig, name));
16297
+ const waker = this.taskBoardWaker = new TaskBoardWaker({
16298
+ deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
16299
+ isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
16300
+ holds: (wake) => db.board.wakeHolds(wake),
16301
+ card: (uuid) => db.board.findCard(uuid),
16302
+ log: (level, message, extra) => this.logger[level](extra ?? {}, message),
16303
+ });
16304
+ db.board.onChange(({ rev, ids, projects, wakes }) => {
16305
+ this.emitSseEvent("tasks", { rev, ids, projects });
16306
+ waker.note(wakes);
16307
+ });
16308
+ }
16309
+ /** A task board notice, as AgEnD's own (not a person's words): through the delivery outbox, held until delivered. */
16310
+ admitTaskBoardNotice(instance, message, key) {
16311
+ this.ensureDeliveryOutbox();
16312
+ const outbox = this.deliveryOutbox;
16313
+ if (!outbox)
16314
+ return false;
16315
+ outbox.admitSystemNotice(instance, key, message, new Date());
16316
+ return true;
16317
+ }
16222
16318
  listClaimedTasks(assignee) {
16223
16319
  try {
16224
16320
  return this.scheduler?.db.listTasks({ assignee, status: "claimed" }) ?? [];
@@ -19188,7 +19284,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19188
19284
  // this is the last moment the prompts can be collapsed.
19189
19285
  await this.retirePendingNoncePrompts();
19190
19286
  this.topicArchiver.stop();
19287
+ this.taskBoardWaker?.stop();
19191
19288
  this.scheduler?.shutdown();
19289
+ // What it was sent is written first (bounded: search-index.ts close()).
19290
+ const search = this.searchIndex;
19291
+ this.searchIndex = null;
19292
+ await search?.close();
19192
19293
  // Stop instances in parallel batches to avoid long sequential waits.
19193
19294
  // Concurrency scales with fleet size — larger fleets tolerate more parallel
19194
19295
  // tmux ops, while small fleets stay conservative to avoid overwhelming the