@markus-global/cli 0.6.3 → 0.6.4

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.
package/dist/markus.mjs CHANGED
@@ -6544,10 +6544,11 @@ var init_context_engine = __esm({
6544
6544
  if (opts.assignedTasks && opts.assignedTasks.length > 0) {
6545
6545
  const priorityOrder = ["critical", "high", "medium", "low"];
6546
6546
  const byPriority = (a, b) => priorityOrder.indexOf(a.priority ?? "medium") - priorityOrder.indexOf(b.priority ?? "medium");
6547
+ const CLOSED_STATUSES = /* @__PURE__ */ new Set(["completed", "cancelled", "failed", "archived", "rejected"]);
6547
6548
  const myTasks = opts.assignedTasks.filter((t) => t.assignedAgentId === opts.agentId);
6548
6549
  const otherTasks = opts.assignedTasks.filter((t) => t.assignedAgentId !== opts.agentId);
6549
- const myActive = myTasks.filter((t) => !["completed", "cancelled", "failed"].includes(t.status)).sort(byPriority);
6550
- const myDone = myTasks.filter((t) => ["completed", "cancelled", "failed"].includes(t.status));
6550
+ const myActive = myTasks.filter((t) => !CLOSED_STATUSES.has(t.status)).sort(byPriority);
6551
+ const myDone = myTasks.filter((t) => CLOSED_STATUSES.has(t.status));
6551
6552
  const MY_TASK_LIMIT = SYSTEM_MY_TASKS_MAX;
6552
6553
  const TEAM_TASK_LIMIT = SYSTEM_TEAM_TASKS_MAX;
6553
6554
  parts.push("\n## Task Board");
@@ -6569,8 +6570,8 @@ var init_context_engine = __esm({
6569
6570
  parts.push(`_(${myDone.length} completed/closed tasks)_`);
6570
6571
  }
6571
6572
  if (otherTasks.length > 0) {
6572
- const otherActive = otherTasks.filter((t) => !["completed", "cancelled", "failed"].includes(t.status)).sort(byPriority);
6573
- const otherDone = otherTasks.filter((t) => ["completed", "cancelled", "failed"].includes(t.status));
6573
+ const otherActive = otherTasks.filter((t) => !CLOSED_STATUSES.has(t.status)).sort(byPriority);
6574
+ const otherDone = otherTasks.filter((t) => CLOSED_STATUSES.has(t.status));
6574
6575
  if (otherActive.length > 0) {
6575
6576
  parts.push("### Team Tasks (assigned to others):");
6576
6577
  const shown = otherActive.slice(0, TEAM_TASK_LIMIT);
@@ -6595,7 +6596,7 @@ var init_context_engine = __esm({
6595
6596
  parts.push("");
6596
6597
  parts.push("**Requirements** (governance gate):");
6597
6598
  parts.push("- `requirement_propose` \u2192 pending human approval \u2192 approved \u2192 link tasks via `requirement_id`");
6598
- parts.push("- When governance requires it, every task MUST reference an approved `requirement_id`.");
6599
+ parts.push("- Every task MUST reference an approved `requirement_id`. Use `requirement_propose` first if no requirement exists.");
6599
6600
  parts.push("");
6600
6601
  parts.push("**Task lifecycle** \u2014 Create \u2192 Execute \u2192 Review \u2192 Complete:");
6601
6602
  parts.push('- **Create**: `task_create` (REQUIRED: `assigned_agent_id`, `reviewer_id`; optional `reviewer_type`: "agent"|"human"). Check `task_list` first to avoid duplicates.');
@@ -6621,7 +6622,8 @@ var init_context_engine = __esm({
6621
6622
  parts.push("- `recall_activity` \u2014 query your own past execution logs by task or activity type. Use when you need to review what you did previously (e.g., to answer a follow-up question).");
6622
6623
  parts.push("");
6623
6624
  parts.push("**Communicating with other agents**:");
6624
- parts.push("- `agent_send_message` \u2014 send a direct message to a peer agent. Use for coordination, questions, sharing context, or instructions. The message enters their mailbox and they will process it.");
6625
+ parts.push("- `agent_send_message` \u2014 send a direct message to a peer agent. **By default this is asynchronous (fire-and-forget)**: the message enters their mailbox and you continue working without waiting. Set `wait_for_reply: true` only when you need the answer before you can proceed (rare \u2014 prefer async).");
6626
+ parts.push("- A2A messaging is inherently **non-blocking**. You send a message, the recipient processes it on their own schedule, and may reply later via their own `agent_send_message`. Do NOT spin-wait or poll for responses.");
6625
6627
  parts.push("- For substantial work requests, create a `task_create` assigned to the target agent instead of asking via message.");
6626
6628
  parts.push("- Do NOT use A2A messages for routine task status notifications \u2014 the system handles those automatically.");
6627
6629
  }
@@ -48860,6 +48862,19 @@ ${escalationReason}`;
48860
48862
  }
48861
48863
  async handleHeartbeat(ctx) {
48862
48864
  log17.info("Processing heartbeat check-in");
48865
+ const queuedNonHeartbeat = this.mailbox.getQueuedItems().filter((i) => i.sourceType !== "heartbeat" && i.status === "queued").length;
48866
+ const fingerprint = `q:${queuedNonHeartbeat}`;
48867
+ if (fingerprint === this.lastHeartbeatFingerprint && queuedNonHeartbeat === 0) {
48868
+ this.consecutiveIdleHeartbeats++;
48869
+ log17.info("Heartbeat: no changes detected, skipping LLM call", {
48870
+ consecutiveIdle: this.consecutiveIdleHeartbeats
48871
+ });
48872
+ this.state.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
48873
+ this.metricsCollector.recordHeartbeat(true);
48874
+ return;
48875
+ }
48876
+ this.lastHeartbeatFingerprint = fingerprint;
48877
+ this.consecutiveIdleHeartbeats = 0;
48863
48878
  const activityId = this.startActivity("heartbeat", "Heartbeat check-in", {});
48864
48879
  let lastHeartbeatSummary = "";
48865
48880
  try {
@@ -49177,12 +49192,15 @@ ${todayLog.slice(0, HEARTBEAT_DAILY_LOG_CHARS)}
49177
49192
  * 2. Memory dream: prune, deduplicate, merge (once per day)
49178
49193
  */
49179
49194
  lastDreamDate = "";
49195
+ lastHeartbeatFingerprint = "";
49196
+ consecutiveIdleHeartbeats = 0;
49180
49197
  async consolidateMemory() {
49181
49198
  try {
49182
49199
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
49183
49200
  const entries2 = this.memory.getEntries();
49184
- if (entries2.length >= 50 && this.lastDreamDate !== today) {
49185
- this.lastDreamDate = today;
49201
+ const dreamKey = entries2.length > 500 ? `${today}_${Math.floor(Date.now() / (6 * 36e5))}` : today;
49202
+ if (entries2.length >= 50 && this.lastDreamDate !== dreamKey) {
49203
+ this.lastDreamDate = dreamKey;
49186
49204
  await this.dreamConsolidateMemory(entries2);
49187
49205
  this.pruneMemoryMd();
49188
49206
  }
@@ -49196,7 +49214,7 @@ ${todayLog.slice(0, HEARTBEAT_DAILY_LOG_CHARS)}
49196
49214
  * outdated items, and merge opportunities. Apply changes programmatically.
49197
49215
  */
49198
49216
  async dreamConsolidateMemory(entries2) {
49199
- const MAX_ENTRIES_FOR_LLM = 200;
49217
+ const MAX_ENTRIES_FOR_LLM = 500;
49200
49218
  const truncated = entries2.length > MAX_ENTRIES_FOR_LLM;
49201
49219
  const batch = truncated ? entries2.slice(-MAX_ENTRIES_FOR_LLM) : entries2;
49202
49220
  const entryList = batch.map((e, i) => {
@@ -49267,8 +49285,8 @@ ${knowledgePreview}
49267
49285
  return;
49268
49286
  }
49269
49287
  const rawPlan = JSON.parse(jsonMatch[0]);
49270
- const MAX_REMOVE_PER_CYCLE = 10;
49271
- const MAX_MERGE_PER_CYCLE = 5;
49288
+ const MAX_REMOVE_PER_CYCLE = 50;
49289
+ const MAX_MERGE_PER_CYCLE = 20;
49272
49290
  const entryIds = new Set(batch.map((e) => e.id));
49273
49291
  const plan = {
49274
49292
  remove: (rawPlan.remove ?? []).filter((id) => entryIds.has(id)).slice(0, MAX_REMOVE_PER_CYCLE),
@@ -49389,6 +49407,52 @@ ${promo.content}` : promo.content;
49389
49407
  if (!inDailyReport)
49390
49408
  afterSectionPrune.push(line);
49391
49409
  }
49410
+ const sections = [];
49411
+ let currentHeading = "";
49412
+ let currentBody = [];
49413
+ let sectionStart = 0;
49414
+ for (let i = 0; i <= afterSectionPrune.length; i++) {
49415
+ const line = i < afterSectionPrune.length ? afterSectionPrune[i] : void 0;
49416
+ const isHeading = line !== void 0 && /^#{1,3}\s/.test(line);
49417
+ if (isHeading || line === void 0) {
49418
+ if (currentHeading || currentBody.length > 0) {
49419
+ sections.push({
49420
+ heading: currentHeading,
49421
+ body: currentBody.join("\n").trim(),
49422
+ startIdx: sectionStart
49423
+ });
49424
+ }
49425
+ currentHeading = line ?? "";
49426
+ currentBody = [];
49427
+ sectionStart = i;
49428
+ } else {
49429
+ currentBody.push(line);
49430
+ }
49431
+ }
49432
+ const headingLastIdx = /* @__PURE__ */ new Map();
49433
+ const normalizeHeading = (h) => h.replace(/^#+\s*/, "").trim().toLowerCase();
49434
+ for (let i = 0; i < sections.length; i++) {
49435
+ const key2 = normalizeHeading(sections[i].heading);
49436
+ if (key2)
49437
+ headingLastIdx.set(key2, i);
49438
+ }
49439
+ const deduped = [];
49440
+ for (let i = 0; i < sections.length; i++) {
49441
+ const s = sections[i];
49442
+ const key2 = normalizeHeading(s.heading);
49443
+ if (key2 && headingLastIdx.get(key2) !== i && headingLastIdx.has(key2)) {
49444
+ continue;
49445
+ }
49446
+ deduped.push(s);
49447
+ }
49448
+ afterSectionPrune.length = 0;
49449
+ for (const s of deduped) {
49450
+ if (s.heading)
49451
+ afterSectionPrune.push(s.heading);
49452
+ if (s.body)
49453
+ afterSectionPrune.push(s.body);
49454
+ afterSectionPrune.push("");
49455
+ }
49392
49456
  const outputLines = [];
49393
49457
  let inThinkBlock = false;
49394
49458
  for (const line of afterSectionPrune) {
@@ -50710,7 +50774,7 @@ function createA2ATools(ctx) {
50710
50774
  type: "string",
50711
50775
  description: 'Required when scope="channel". The channel key (e.g., "group:<teamId>" for team chats, "dm:<id1>_<id2>" for DMs).'
50712
50776
  },
50713
- limit: { type: "number", description: "Number of items to fetch (default 30, max 50)." },
50777
+ limit: { type: "number", description: "Number of items to fetch (default 80, max 200)." },
50714
50778
  before: { type: "string", description: "ISO timestamp \u2014 fetch items older than this for pagination. Omit for most recent." }
50715
50779
  },
50716
50780
  required: ["scope"]
@@ -50722,11 +50786,11 @@ function createA2ATools(ctx) {
50722
50786
  if (!channelKey) {
50723
50787
  return JSON.stringify({ status: "error", error: 'channel_key is required when scope="channel"' });
50724
50788
  }
50725
- const limit = Math.min(args["limit"] ?? 30, 50);
50789
+ const limit = Math.min(args["limit"] ?? 80, 200);
50726
50790
  const before2 = args["before"];
50727
50791
  try {
50728
50792
  const result = await ctx.getChannelMessages(channelKey, limit, before2);
50729
- const formatted = result.messages.map((m) => `[${m.createdAt}] ${m.senderType === "agent" ? `[agent] ${m.senderName}` : `[human] ${m.senderName}`}: ${m.text.slice(0, 500)}`);
50793
+ const formatted = result.messages.map((m) => `[${m.createdAt}] ${m.senderType === "agent" ? `[agent] ${m.senderName}` : `[human] ${m.senderName}`}: ${m.text.slice(0, 2e3)}`);
50730
50794
  return JSON.stringify({
50731
50795
  messages: formatted,
50732
50796
  count: result.messages.length,
@@ -51182,6 +51246,15 @@ function createAgentTaskTools(ctx) {
51182
51246
  items: { type: "string" },
51183
51247
  description: "Update the list of task IDs that block this task. Only the task creator can modify this field. Pass an empty array to clear all blockers."
51184
51248
  },
51249
+ reviewer_id: {
51250
+ type: "string",
51251
+ description: "New reviewer agent or human ID. Only the task creator or a manager can change the reviewer."
51252
+ },
51253
+ reviewer_type: {
51254
+ type: "string",
51255
+ enum: ["agent", "human"],
51256
+ description: "Whether the new reviewer is an agent or a human user. Required when changing reviewer_id."
51257
+ },
51185
51258
  schedule: {
51186
51259
  type: "object",
51187
51260
  description: 'Update the schedule for a scheduled (recurring) task. Only works on tasks with taskType "scheduled". Set either "every" OR "cron", not both.',
@@ -51202,20 +51275,30 @@ function createAgentTaskTools(ctx) {
51202
51275
  const note = args["note"];
51203
51276
  const description = args["description"];
51204
51277
  const blockedBy = args["blocked_by"];
51205
- if (blockedBy !== void 0) {
51278
+ const reviewerId = args["reviewer_id"];
51279
+ const reviewerType = args["reviewer_type"];
51280
+ if (blockedBy !== void 0 || reviewerId !== void 0) {
51206
51281
  const existing = ctx.getTask ? await ctx.getTask(taskId2) : null;
51207
51282
  const createdBy = existing?.["createdBy"];
51208
- if (createdBy && createdBy !== ctx.agentId) {
51283
+ if (blockedBy !== void 0 && createdBy && createdBy !== ctx.agentId) {
51209
51284
  return JSON.stringify({
51210
51285
  status: "denied",
51211
51286
  error: "Only the task creator can modify blocked_by. You are not the creator of this task."
51212
51287
  });
51213
51288
  }
51289
+ if (reviewerId !== void 0 && createdBy && createdBy !== ctx.agentId) {
51290
+ return JSON.stringify({
51291
+ status: "denied",
51292
+ error: "Only the task creator or a manager can change the reviewer."
51293
+ });
51294
+ }
51214
51295
  }
51215
- if ((description !== void 0 || blockedBy !== void 0) && ctx.updateTaskFields) {
51296
+ if ((description !== void 0 || blockedBy !== void 0 || reviewerId !== void 0) && ctx.updateTaskFields) {
51216
51297
  await ctx.updateTaskFields(taskId2, {
51217
51298
  ...description !== void 0 ? { description } : {},
51218
- ...blockedBy !== void 0 ? { blockedBy } : {}
51299
+ ...blockedBy !== void 0 ? { blockedBy } : {},
51300
+ ...reviewerId !== void 0 ? { reviewerId } : {},
51301
+ ...reviewerType !== void 0 ? { reviewerType } : {}
51219
51302
  });
51220
51303
  }
51221
51304
  const schedule = args["schedule"];
@@ -51404,7 +51487,7 @@ function createAgentTaskTools(ctx) {
51404
51487
  ...ctx.addTaskNote ? [
51405
51488
  {
51406
51489
  name: "task_note",
51407
- description: "Add a progress note or comment to a task without changing its status. Use this to log intermediate findings, decisions, or observations while working on a task.",
51490
+ description: "Add a one-way progress note to a task's timeline log without changing its status. Use this to record intermediate findings, decisions, or milestones. For interactive discussion with other agents or humans, use task_comment instead.",
51408
51491
  inputSchema: {
51409
51492
  type: "object",
51410
51493
  properties: {
@@ -52642,6 +52725,48 @@ ${content}` : content;
52642
52725
  log24.info("Agent updated long-term memory", { agentId: ctx.agentId, section: section4, mode, contentLen: content.length });
52643
52726
  return JSON.stringify({ status: "updated", section: section4, mode });
52644
52727
  }
52728
+ },
52729
+ {
52730
+ name: "memory_delete",
52731
+ description: "Delete specific entries from your memory buffer (memories.json). Use this to clean up outdated, incorrect, or redundant observations. Provide either a list of entry IDs (from memory_list/memory_search) or a tag to remove all entries with that tag. Maximum 20 entries per call.",
52732
+ inputSchema: {
52733
+ type: "object",
52734
+ properties: {
52735
+ ids: {
52736
+ type: "array",
52737
+ items: { type: "string" },
52738
+ description: "Array of memory entry IDs to delete. Use memory_list or memory_search to find IDs."
52739
+ },
52740
+ tag: {
52741
+ type: "string",
52742
+ description: "Delete all entries with this tag. Alternative to specifying individual IDs."
52743
+ }
52744
+ }
52745
+ },
52746
+ async execute(args) {
52747
+ const ids = args["ids"];
52748
+ const tag = args["tag"];
52749
+ if (!ids?.length && !tag) {
52750
+ return JSON.stringify({ status: "error", error: "Provide either ids or tag to delete." });
52751
+ }
52752
+ const MAX_DELETE = 20;
52753
+ let removed = 0;
52754
+ if (ids?.length) {
52755
+ const capped = ids.slice(0, MAX_DELETE);
52756
+ removed = ctx.memory.removeEntries(capped);
52757
+ if (ctx.semanticSearch?.isEnabled()) {
52758
+ for (const id of capped) {
52759
+ ctx.semanticSearch.deleteMemory(id).catch((err) => {
52760
+ log24.warn("Failed to remove memory from semantic index", { error: String(err) });
52761
+ });
52762
+ }
52763
+ }
52764
+ } else if (tag) {
52765
+ removed = ctx.memory.removeEntriesByTag(tag);
52766
+ }
52767
+ log24.info("Agent deleted memories", { agentId: ctx.agentId, removed, byTag: tag ?? null });
52768
+ return JSON.stringify({ status: "deleted", removed });
52769
+ }
52645
52770
  }
52646
52771
  ];
52647
52772
  }
@@ -62407,14 +62532,15 @@ ${c.content}`;
62407
62532
  reviewer: task.reviewerId
62408
62533
  });
62409
62534
  if (this.hitlService && request.creatorRole !== "human" && task.status === "pending") {
62410
- const creatorName = request.createdBy ?? "unknown agent";
62535
+ const creatorId = request.createdBy ?? "system";
62536
+ const creatorName = this.resolveActorName(creatorId, "agent") ?? creatorId;
62411
62537
  this.hitlService.requestApprovalAndWait({
62412
- agentId: request.createdBy ?? "system",
62538
+ agentId: creatorId,
62413
62539
  agentName: creatorName,
62414
62540
  type: "custom",
62415
- title: `Task approval: ${task.title}`,
62541
+ title: task.title,
62416
62542
  description: `Agent "${creatorName}" wants to create task "${task.title}" (priority: ${task.priority}).`,
62417
- details: { taskId: task.id, priority: task.priority }
62543
+ details: { taskId: task.id, priority: task.priority, subType: "task" }
62418
62544
  }).then((result) => {
62419
62545
  const current = this.tasks.get(task.id);
62420
62546
  if (!current || current.status !== "pending")
@@ -62701,9 +62827,10 @@ ${c.content}`;
62701
62827
  Deliverables:
62702
62828
  ${deliverablesSummary}` : ""
62703
62829
  ].filter(Boolean).join("\n");
62830
+ const assigneeName = this.resolveActorName(task.assignedAgentId, "agent") ?? task.assignedAgentId;
62704
62831
  this.hitlService.requestApprovalAndWait({
62705
62832
  agentId: task.assignedAgentId,
62706
- agentName: task.assignedAgentId,
62833
+ agentName: assigneeName,
62707
62834
  type: "custom",
62708
62835
  title: `Review: ${task.title}`,
62709
62836
  description,
@@ -63061,6 +63188,12 @@ Action: ${guidance}` : ""
63061
63188
  if (data.reviewerType !== void 0)
63062
63189
  task.reviewerType = data.reviewerType;
63063
63190
  if (data.blockedBy !== void 0) {
63191
+ if (data.blockedBy.length > 0) {
63192
+ const cycle = this.detectBlockedByCycle(id, data.blockedBy);
63193
+ if (cycle) {
63194
+ throw new Error(`Circular dependency detected: ${cycle.join(" \u2192 ")}. Cannot set blocked_by \u2014 this would create a deadlock.`);
63195
+ }
63196
+ }
63064
63197
  task.blockedBy = data.blockedBy;
63065
63198
  if (this.taskRepo && "updateBlockedBy" in this.taskRepo) {
63066
63199
  this.taskRepo.updateBlockedBy(id, data.blockedBy).catch((err) => log50.warn("Failed to persist blockedBy to DB", { error: String(err) }));
@@ -63185,6 +63318,31 @@ Action: ${guidance}` : ""
63185
63318
  this.cascadeCancelDependents(task);
63186
63319
  }
63187
63320
  }
63321
+ /**
63322
+ * Detect cycles in blocked_by dependencies using BFS.
63323
+ * Returns the cycle path if found, or null if no cycle exists.
63324
+ */
63325
+ detectBlockedByCycle(taskId2, proposedBlockers) {
63326
+ for (const blockerId of proposedBlockers) {
63327
+ const visited = /* @__PURE__ */ new Set();
63328
+ const queue = [{ id: blockerId, path: [taskId2, blockerId] }];
63329
+ while (queue.length > 0) {
63330
+ const { id: current, path } = queue.shift();
63331
+ if (current === taskId2)
63332
+ return path;
63333
+ if (visited.has(current))
63334
+ continue;
63335
+ visited.add(current);
63336
+ const blockerTask = this.tasks.get(current);
63337
+ if (blockerTask?.blockedBy) {
63338
+ for (const nextId of blockerTask.blockedBy) {
63339
+ queue.push({ id: nextId, path: [...path, nextId] });
63340
+ }
63341
+ }
63342
+ }
63343
+ }
63344
+ return null;
63345
+ }
63188
63346
  areBlockersSatisfied(task) {
63189
63347
  if (!task.blockedBy?.length)
63190
63348
  return true;
@@ -65943,7 +66101,7 @@ var init_api_server = __esm({
65943
66101
  let channelContext = [];
65944
66102
  if (this.storage) {
65945
66103
  try {
65946
- const recent = await this.storage.channelMessageRepo.getMessages(channelKey, 20);
66104
+ const recent = await this.storage.channelMessageRepo.getMessages(channelKey, 80);
65947
66105
  channelContext = (recent.messages ?? []).map((m) => ({
65948
66106
  role: m.senderType === "agent" ? "assistant" : "user",
65949
66107
  content: m.senderType === "agent" ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`
@@ -66513,13 +66671,33 @@ ${cleanText}`,
66513
66671
  toolEventCollector: toolEvents,
66514
66672
  waitForReply: isA2A ? true : void 0
66515
66673
  });
66674
+ const emitNoResponse = () => {
66675
+ const evt = {
66676
+ type: "chat:agent_no_response",
66677
+ payload: { channel, agentId: agentId2 },
66678
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
66679
+ };
66680
+ if (channel.startsWith("dm:") || channel.startsWith("notes:")) {
66681
+ const parts = channel.startsWith("notes:") ? [channel.slice(6)] : channel.slice(3).split(":");
66682
+ this.ws.sendToUsers(parts, evt);
66683
+ } else {
66684
+ const humanIds = this.resolveChannelHumanIds(channel);
66685
+ if (humanIds.length > 0)
66686
+ this.ws.sendToUsers(humanIds, evt);
66687
+ else
66688
+ this.ws.broadcast(evt);
66689
+ }
66690
+ };
66516
66691
  if (!reply || !reply.trim() || reply.includes("[NO_RESPONSE]")) {
66692
+ emitNoResponse();
66517
66693
  return;
66518
66694
  }
66519
66695
  const { thinking, clean: rawClean } = extractThinkBlocks(reply);
66520
66696
  const cleanReply = rawClean.replace(/\[NO_RESPONSE\]/gi, "").trim();
66521
- if (!cleanReply)
66697
+ if (!cleanReply) {
66698
+ emitNoResponse();
66522
66699
  return;
66700
+ }
66523
66701
  const metadata = {};
66524
66702
  if (thinking.length > 0)
66525
66703
  metadata["thinking"] = thinking;
@@ -66622,7 +66800,7 @@ ${cleanText}`,
66622
66800
  let channelContext = [];
66623
66801
  if (this.storage) {
66624
66802
  try {
66625
- const recent = await this.storage.channelMessageRepo.getMessages(channel, 20);
66803
+ const recent = await this.storage.channelMessageRepo.getMessages(channel, 80);
66626
66804
  channelContext = (recent.messages ?? []).map((m) => ({
66627
66805
  role: m.senderType === "agent" ? "assistant" : "user",
66628
66806
  content: m.senderType === "agent" ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`
@@ -67162,7 +67340,7 @@ ${cleanText}`,
67162
67340
  if (!this.storage)
67163
67341
  return [];
67164
67342
  try {
67165
- const recent = await this.storage.channelMessageRepo.getMessages(channel, 20);
67343
+ const recent = await this.storage.channelMessageRepo.getMessages(channel, 80);
67166
67344
  return (recent.messages ?? []).map((m) => ({
67167
67345
  role: m.senderType === "agent" ? "assistant" : "user",
67168
67346
  content: m.senderType === "agent" ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`
@@ -71345,6 +71523,50 @@ EXPLANATION_END`;
71345
71523
  this.json(res, 200, { success: read ?? false });
71346
71524
  return;
71347
71525
  }
71526
+ if (path === "/api/activity" && req.method === "GET") {
71527
+ const authUser = await this.requireAuth(req, res);
71528
+ if (!authUser)
71529
+ return;
71530
+ const userId2 = authUser.userId;
71531
+ const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "50", 10), 200);
71532
+ const typeFilter = url.searchParams.get("type") ?? void 0;
71533
+ const items = [];
71534
+ if (!typeFilter || typeFilter === "notification") {
71535
+ const notifications = this.hitlService?.listNotifications(userId2, false, { limit }) ?? [];
71536
+ for (const n of notifications) {
71537
+ items.push({
71538
+ id: n.id,
71539
+ type: n.type,
71540
+ title: n.title,
71541
+ body: n.body,
71542
+ timestamp: n.createdAt,
71543
+ source: "notification",
71544
+ metadata: n.metadata
71545
+ });
71546
+ }
71547
+ }
71548
+ if ((!typeFilter || typeFilter === "task_comment") && this.storage?.taskCommentRepo) {
71549
+ try {
71550
+ const recentComments = this.storage.taskCommentRepo.listRecent?.(limit) ?? [];
71551
+ for (const c of recentComments) {
71552
+ items.push({
71553
+ id: c.id,
71554
+ type: "task_comment",
71555
+ title: `Comment on task ${c.taskId}`,
71556
+ body: typeof c.body === "string" ? c.body.slice(0, 300) : String(c.body ?? ""),
71557
+ timestamp: c.createdAt,
71558
+ source: "task_comment",
71559
+ metadata: { taskId: c.taskId, authorId: c.authorId, authorName: c.authorName }
71560
+ });
71561
+ }
71562
+ } catch {
71563
+ }
71564
+ }
71565
+ items.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
71566
+ const page = items.slice(0, limit);
71567
+ this.json(res, 200, { items: page, totalCount: items.length });
71568
+ return;
71569
+ }
71348
71570
  if (path === "/api/usage" && req.method === "GET") {
71349
71571
  const orgId2 = url.searchParams.get("orgId") ?? "default";
71350
71572
  const plan = this.billingService?.getOrgPlan(orgId2);
@@ -73980,6 +74202,8 @@ EXPLANATION_END`;
73980
74202
  exact("/api/notifications", "GET"),
73981
74203
  exact("/api/notifications/mark-all-read", "POST"),
73982
74204
  startsWith("/api/notifications/", "POST"),
74205
+ // ── Activity feed ─────────────────────────────────────────────────
74206
+ exact("/api/activity", "GET"),
73983
74207
  // ── Users ────────────────────────────────────────────────────────────
73984
74208
  exact("/api/users", "GET", "POST"),
73985
74209
  regex(/^\/api\/users\/[^/]+$/, "PATCH"),
@@ -74595,7 +74819,7 @@ var init_hitl_service = __esm({
74595
74819
  this.notify({
74596
74820
  targetUserId: opts.targetUserId ?? "all",
74597
74821
  type: "approval_request",
74598
- title: `Approval needed: ${opts.title}`,
74822
+ title: opts.title,
74599
74823
  body: opts.description,
74600
74824
  priority: "high",
74601
74825
  actionType: "navigate",
@@ -75469,13 +75693,14 @@ var init_requirement_service = __esm({
75469
75693
  }
75470
75694
  this.broadcast("requirement:created", req);
75471
75695
  if (this.hitlService && req.source === "agent") {
75696
+ const creatorName = this.resolveAgentName(req.createdBy);
75472
75697
  this.hitlService.requestApprovalAndWait({
75473
75698
  agentId: req.createdBy,
75474
- agentName: req.createdBy,
75699
+ agentName: creatorName,
75475
75700
  type: "custom",
75476
- title: `Requirement approval: ${req.title}`,
75477
- description: `Agent "${req.createdBy}" proposed requirement "${req.title}" (priority: ${req.priority}).`,
75478
- details: { requirementId: req.id, priority: req.priority },
75701
+ title: req.title,
75702
+ description: `Agent "${creatorName}" proposed requirement "${req.title}" (priority: ${req.priority}).`,
75703
+ details: { requirementId: req.id, priority: req.priority, subType: "requirement" },
75479
75704
  targetUserId: "all"
75480
75705
  }).then((result) => {
75481
75706
  const current = this.requirements.get(req.id);
@@ -75611,13 +75836,14 @@ var init_requirement_service = __esm({
75611
75836
  this.broadcast("requirement:resubmitted", req);
75612
75837
  log61.info("Requirement resubmitted for review", { id, hasUpdates: !!updates });
75613
75838
  if (this.hitlService && req.source === "agent") {
75839
+ const creatorName = this.resolveAgentName(req.createdBy);
75614
75840
  this.hitlService.requestApprovalAndWait({
75615
75841
  agentId: req.createdBy,
75616
- agentName: req.createdBy,
75842
+ agentName: creatorName,
75617
75843
  type: "custom",
75618
- title: `Requirement approval (resubmitted): ${req.title}`,
75619
- description: `Agent "${req.createdBy}" resubmitted requirement "${req.title}" (priority: ${req.priority}).`,
75620
- details: { requirementId: req.id, priority: req.priority },
75844
+ title: req.title,
75845
+ description: `Agent "${creatorName}" resubmitted requirement "${req.title}" (priority: ${req.priority}).`,
75846
+ details: { requirementId: req.id, priority: req.priority, subType: "requirement_resubmit" },
75621
75847
  targetUserId: "all"
75622
75848
  }).then((result) => {
75623
75849
  const current = this.requirements.get(req.id);
@@ -75955,6 +76181,17 @@ var init_requirement_service = __esm({
75955
76181
  });
75956
76182
  }
75957
76183
  }
76184
+ resolveAgentName(agentId2) {
76185
+ if (this.agentManager) {
76186
+ try {
76187
+ const agent = this.agentManager.getAgent(agentId2);
76188
+ if (agent)
76189
+ return agent.config?.name ?? agent.name ?? agentId2;
76190
+ } catch {
76191
+ }
76192
+ }
76193
+ return agentId2;
76194
+ }
75958
76195
  broadcast(type, data) {
75959
76196
  if (this.ws) {
75960
76197
  this.ws.broadcast({
@@ -77106,13 +77343,19 @@ var init_stale_detector = __esm({
77106
77343
  taskService;
77107
77344
  config;
77108
77345
  scanInterval;
77109
- constructor(taskService, config) {
77346
+ onStaleItems;
77347
+ constructor(taskService, config, onStaleItems) {
77110
77348
  this.taskService = taskService;
77111
77349
  this.config = { ...DEFAULT_CONFIG4, ...config };
77350
+ this.onStaleItems = onStaleItems;
77112
77351
  }
77113
77352
  start(intervalMs = 36e5) {
77114
77353
  this.scanInterval = setInterval(() => {
77115
- this.scan().catch((err) => log68.warn("Stale scan failed", { error: String(err) }));
77354
+ this.scan().then((items) => {
77355
+ if (items.length > 0 && this.onStaleItems) {
77356
+ this.onStaleItems(items);
77357
+ }
77358
+ }).catch((err) => log68.warn("Stale scan failed", { error: String(err) }));
77116
77359
  }, intervalMs);
77117
77360
  log68.info("Stale detector started", { intervalMs });
77118
77361
  }
@@ -82442,6 +82685,21 @@ async function startServer(config, values) {
82442
82685
  const archiveService = new ArchiveService(taskService, projectService);
82443
82686
  archiveService.setRequirementService(requirementService);
82444
82687
  archiveService.start();
82688
+ const staleDetector = new StaleDetector(taskService, void 0, (items) => {
82689
+ for (const item of items) {
82690
+ hitlService.notify({
82691
+ targetUserId: "all",
82692
+ type: "system",
82693
+ title: item.type === "review_stale" ? "Stale review" : item.type === "stuck_task" ? "Stuck task" : "Unstarted task",
82694
+ body: item.message,
82695
+ priority: item.type === "review_stale" ? "high" : "normal",
82696
+ actionType: item.taskId ? "navigate" : "none",
82697
+ actionTarget: item.taskId ? JSON.stringify({ path: `/work?openTask=${item.taskId}` }) : void 0,
82698
+ metadata: { taskId: item.taskId, agentId: item.agentId, staleType: item.type }
82699
+ });
82700
+ }
82701
+ });
82702
+ staleDetector.start();
82445
82703
  apiServer.setLLMRouter(llmRouter);
82446
82704
  apiServer.setConfigPath(values["config"] ?? getDefaultConfigPath());
82447
82705
  if (config.hub?.url) apiServer.setHubUrl(config.hub.url);
@@ -83278,6 +83536,7 @@ ${reason}`;
83278
83536
  closeStartupLogger();
83279
83537
  closeRuntimeLogger();
83280
83538
  archiveService.stop();
83539
+ staleDetector.stop();
83281
83540
  scheduledTaskRunner.stop();
83282
83541
  apiServer.stop();
83283
83542
  agentManager.shutdown().then(() => messageRouter.disconnectAll()).then(() => process.exit(0)).catch(() => process.exit(1));