@mclean-capital/neura 3.5.2 → 3.5.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.
@@ -8013,6 +8013,7 @@ __export(work_item_queries_exports, {
8013
8013
  deleteWorkItem: () => deleteWorkItem,
8014
8014
  getOpenWorkItems: () => getOpenWorkItems,
8015
8015
  getWorkItem: () => getWorkItem,
8016
+ getWorkItemByWorkerId: () => getWorkItemByWorkerId,
8016
8017
  getWorkItems: () => getWorkItems,
8017
8018
  updateWorkItem: () => updateWorkItem
8018
8019
  });
@@ -8051,6 +8052,13 @@ async function getWorkItem(db, id) {
8051
8052
  const result = await db.query("SELECT * FROM work_items WHERE id = $1", [id]);
8052
8053
  return result.rows.length > 0 ? mapWorkItem(result.rows[0]) : null;
8053
8054
  }
8055
+ async function getWorkItemByWorkerId(db, workerId) {
8056
+ const result = await db.query(
8057
+ "SELECT * FROM work_items WHERE worker_id = $1 LIMIT 1",
8058
+ [workerId]
8059
+ );
8060
+ return result.rows.length > 0 ? mapWorkItem(result.rows[0]) : null;
8061
+ }
8054
8062
  async function createWorkItem(db, title, priority, options) {
8055
8063
  const id = crypto3.randomUUID();
8056
8064
  await db.query(
@@ -73035,7 +73043,9 @@ function loadNeuraSkills(options = {}) {
73035
73043
  const repoLocal = resolve(cwd, ".neura", "skills");
73036
73044
  const global2 = options.globalSkillsDir ?? resolve(homedir2(), ".neura", "skills");
73037
73045
  const explicit = options.explicitPaths ?? [];
73038
- const skillPaths = [repoLocal, global2, ...explicit];
73046
+ const skillPaths = [repoLocal, global2];
73047
+ if (options.bundledSkillsDir) skillPaths.push(options.bundledSkillsDir);
73048
+ skillPaths.push(...explicit);
73039
73049
  log12.info("loading Neura skills (repo-local \u2192 global \u2192 explicit, pi defaults excluded)", {
73040
73050
  cwd,
73041
73051
  skillPaths
@@ -73142,6 +73152,7 @@ var SkillWatcher = class {
73142
73152
  registry;
73143
73153
  cwd;
73144
73154
  globalSkillsDir;
73155
+ bundledSkillsDir;
73145
73156
  explicitPaths;
73146
73157
  debounceMs;
73147
73158
  onReload;
@@ -73153,6 +73164,7 @@ var SkillWatcher = class {
73153
73164
  this.registry = options.registry;
73154
73165
  this.cwd = options.cwd ?? process.cwd();
73155
73166
  this.globalSkillsDir = options.globalSkillsDir ?? resolve2(homedir3(), ".neura", "skills");
73167
+ this.bundledSkillsDir = options.bundledSkillsDir;
73156
73168
  this.explicitPaths = options.explicitPaths ?? [];
73157
73169
  this.debounceMs = options.debounceMs ?? 200;
73158
73170
  this.onReload = options.onReload;
@@ -73165,7 +73177,9 @@ var SkillWatcher = class {
73165
73177
  if (this.watcher) return;
73166
73178
  this.reload();
73167
73179
  const repoLocal = resolve2(this.cwd, ".neura", "skills");
73168
- const paths = [repoLocal, this.globalSkillsDir, ...this.explicitPaths];
73180
+ const paths = [repoLocal, this.globalSkillsDir];
73181
+ if (this.bundledSkillsDir) paths.push(this.bundledSkillsDir);
73182
+ paths.push(...this.explicitPaths);
73169
73183
  log13.info("starting skill watcher", { paths, debounceMs: this.debounceMs });
73170
73184
  this.watcher = chokidar.watch(paths, {
73171
73185
  ignoreInitial: true,
@@ -73241,6 +73255,7 @@ var SkillWatcher = class {
73241
73255
  const result = loadNeuraSkills({
73242
73256
  cwd: this.cwd,
73243
73257
  globalSkillsDir: this.globalSkillsDir,
73258
+ bundledSkillsDir: this.bundledSkillsDir,
73244
73259
  explicitPaths: this.explicitPaths
73245
73260
  });
73246
73261
  this.registry.replaceAll(result.skills);
@@ -76084,6 +76099,18 @@ var MEMORY_NAMES = /* @__PURE__ */ new Set([
76084
76099
  "memory_stats"
76085
76100
  ]);
76086
76101
 
76102
+ // src/tools/voice-redact.ts
76103
+ function redactTaskForVoice(task) {
76104
+ const { workerId, ...rest } = task;
76105
+ return { ...rest, hasActiveWorker: workerId !== null };
76106
+ }
76107
+ function redactCommentForVoice(comment) {
76108
+ if (comment.author.startsWith("worker:")) {
76109
+ return { ...comment, author: "worker" };
76110
+ }
76111
+ return comment;
76112
+ }
76113
+
76087
76114
  // src/tools/task-tools.ts
76088
76115
  var log15 = new Logger("tool:task");
76089
76116
  var ALL_STATUSES = [
@@ -76099,7 +76126,6 @@ var ALL_STATUSES = [
76099
76126
  ];
76100
76127
  var COMMENT_TYPES = [
76101
76128
  "progress",
76102
- "heartbeat",
76103
76129
  "clarification_request",
76104
76130
  "approval_request",
76105
76131
  "clarification_response",
@@ -76199,7 +76225,7 @@ var taskToolDefs = [
76199
76225
  {
76200
76226
  type: "function",
76201
76227
  name: "update_task",
76202
- description: "Update a task: field changes, status transitions, and/or comment appends (unified with the worker protocol). Workers use this for report_progress / heartbeat / request_clarification / request_approval / complete_task / fail_task. Orchestrator uses it for user responses (clarification_response / approval_response) and field edits.",
76228
+ description: "Update a task: field changes, status transitions, and/or comment appends (unified with the worker protocol). Workers use this for report_progress / request_clarification / request_approval / complete_task / fail_task. Orchestrator uses it for user responses (clarification_response / approval_response) and field edits.",
76203
76229
  parameters: {
76204
76230
  type: "object",
76205
76231
  properties: {
@@ -76325,7 +76351,12 @@ async function handleTaskTool(name, args, ctx) {
76325
76351
  goal: t2.goal,
76326
76352
  source: t2.source,
76327
76353
  version: t2.version,
76328
- workerId: t2.workerId
76354
+ // workerId deliberately omitted from the voice-facing
76355
+ // listing — TTS reads UUIDs letter-by-letter. Callers
76356
+ // that need the worker id should go through
76357
+ // list_active_workers (which runs internal tool calls
76358
+ // that don't narrate).
76359
+ hasActiveWorker: t2.workerId !== null
76329
76360
  }))
76330
76361
  }
76331
76362
  };
@@ -76337,14 +76368,25 @@ async function handleTaskTool(name, args, ctx) {
76337
76368
  if (!task) return { result: { found: false } };
76338
76369
  let comments = [];
76339
76370
  try {
76340
- comments = await ctx.taskTools.listTaskComments(task.id, { limit: 50 });
76371
+ const recentDesc = await ctx.taskTools.listTaskComments(task.id, {
76372
+ limit: 50,
76373
+ order: "desc",
76374
+ excludeTypes: ["heartbeat"]
76375
+ });
76376
+ comments = recentDesc.reverse();
76341
76377
  } catch (err) {
76342
- log15.warn("failed to load task comments for get_task", {
76378
+ log15.error("failed to load task comments for get_task", {
76343
76379
  taskId: task.id,
76344
76380
  err: String(err)
76345
76381
  });
76346
76382
  }
76347
- return { result: { found: true, task, comments } };
76383
+ return {
76384
+ result: {
76385
+ found: true,
76386
+ task: redactTaskForVoice(task),
76387
+ comments: comments.map(redactCommentForVoice)
76388
+ }
76389
+ };
76348
76390
  }
76349
76391
  case "update_task": {
76350
76392
  if (!ctx.taskTools) return { error: "Task system not available" };
@@ -76923,6 +76965,8 @@ import {
76923
76965
  SessionManager
76924
76966
  } from "@mariozechner/pi-coding-agent";
76925
76967
  var log19 = new Logger("pi-runtime");
76968
+ var ALIVE_PULSE_THROTTLE_MS = 3e4;
76969
+ var ALIVE_INTERVAL_MS = 6e4;
76926
76970
  var BRIDGE_EVENT_TYPES = /* @__PURE__ */ new Set([
76927
76971
  "agent_start",
76928
76972
  "agent_end",
@@ -76993,6 +77037,15 @@ var PiRuntime = class {
76993
77037
  handleAgentEvent(workerId, event) {
76994
77038
  const worker = this.active.get(workerId);
76995
77039
  if (!worker) return;
77040
+ const now = Date.now();
77041
+ if (now - worker.lastAliveAt >= ALIVE_PULSE_THROTTLE_MS) {
77042
+ worker.lastAliveAt = now;
77043
+ try {
77044
+ worker.callbacks.onAlive?.();
77045
+ } catch (err) {
77046
+ log19.warn("onAlive callback threw", { workerId, err: String(err) });
77047
+ }
77048
+ }
76996
77049
  if (event.type === "agent_start") {
76997
77050
  worker.callbacks.onStatusChange?.("running");
76998
77051
  } else if (event.type === "agent_end") {
@@ -77026,6 +77079,10 @@ var PiRuntime = class {
77026
77079
  worker.callbacks.onStatusChange?.(finalStatus);
77027
77080
  worker.callbacks.onComplete?.(result);
77028
77081
  worker.resolveDone(result);
77082
+ if (worker.aliveInterval) {
77083
+ clearInterval(worker.aliveInterval);
77084
+ worker.aliveInterval = void 0;
77085
+ }
77029
77086
  if (finalStatus !== "idle_partial") {
77030
77087
  this.active.delete(workerId);
77031
77088
  } else {
@@ -77034,6 +77091,28 @@ var PiRuntime = class {
77034
77091
  }
77035
77092
  return result;
77036
77093
  }
77094
+ /**
77095
+ * Start the defense-in-depth interval that refreshes the worker's
77096
+ * lease on a timer, independent of pi's event stream. Pi's bash
77097
+ * tool (and any custom tool that doesn't stream) can go silent for
77098
+ * minutes between `tool_execution_start` and `_end`; without this
77099
+ * pulse the lease would expire and crash-recovery could kill a
77100
+ * perfectly healthy worker that's just waiting on a subprocess.
77101
+ * `unref()` so the timer doesn't keep the process alive on its own.
77102
+ */
77103
+ startAliveInterval(worker) {
77104
+ worker.aliveInterval = setInterval(() => {
77105
+ try {
77106
+ worker.callbacks.onAlive?.();
77107
+ } catch (err) {
77108
+ log19.warn("interval onAlive callback threw", {
77109
+ workerId: worker.workerId,
77110
+ err: String(err)
77111
+ });
77112
+ }
77113
+ }, ALIVE_INTERVAL_MS);
77114
+ worker.aliveInterval.unref?.();
77115
+ }
77037
77116
  /**
77038
77117
  * Authoritative `stopReason` → `WorkerStatus` mapping. Matches the
77039
77118
  * table in docs/phase6-os-core.md exactly:
@@ -77084,9 +77163,11 @@ var PiRuntime = class {
77084
77163
  resolveDone,
77085
77164
  idleWaiters: [],
77086
77165
  pendingPause: false,
77087
- callbacks
77166
+ callbacks,
77167
+ lastAliveAt: 0
77088
77168
  };
77089
77169
  this.active.set(workerId, worker);
77170
+ this.startAliveInterval(worker);
77090
77171
  session.prompt(task.description).then(() => {
77091
77172
  const stopReason = this.extractStopReasonFromSession(session);
77092
77173
  this.finalizeWorker(workerId, stopReason);
@@ -77120,9 +77201,11 @@ var PiRuntime = class {
77120
77201
  resolveDone,
77121
77202
  idleWaiters: [],
77122
77203
  pendingPause: false,
77123
- callbacks
77204
+ callbacks,
77205
+ lastAliveAt: 0
77124
77206
  };
77125
77207
  this.active.set(workerId, worker);
77208
+ this.startAliveInterval(worker);
77126
77209
  session.prompt(resumePrompt).then(() => {
77127
77210
  const stopReason = this.extractStopReasonFromSession(session);
77128
77211
  this.finalizeWorker(workerId, stopReason);
@@ -77395,6 +77478,7 @@ async function insertComment(db, opts) {
77395
77478
  }
77396
77479
  async function listComments(db, opts) {
77397
77480
  const limit2 = opts.limit ?? 500;
77481
+ const order = opts.order ?? "asc";
77398
77482
  const filters = ["task_id = $1"];
77399
77483
  const values = [opts.taskId];
77400
77484
  let idx = 2;
@@ -77403,6 +77487,10 @@ async function listComments(db, opts) {
77403
77487
  const placeholders = types3.map(() => `$${idx++}`).join(", ");
77404
77488
  filters.push(`type IN (${placeholders})`);
77405
77489
  values.push(...types3);
77490
+ } else if (opts.excludeTypes && opts.excludeTypes.length > 0) {
77491
+ const placeholders = opts.excludeTypes.map(() => `$${idx++}`).join(", ");
77492
+ filters.push(`type NOT IN (${placeholders})`);
77493
+ values.push(...opts.excludeTypes);
77406
77494
  }
77407
77495
  if (opts.since) {
77408
77496
  filters.push(`created_at > $${idx++}`);
@@ -77411,7 +77499,7 @@ async function listComments(db, opts) {
77411
77499
  const result = await db.query(
77412
77500
  `SELECT * FROM task_comments
77413
77501
  WHERE ${filters.join(" AND ")}
77414
- ORDER BY created_at ASC
77502
+ ORDER BY created_at ${order === "desc" ? "DESC" : "ASC"}
77415
77503
  LIMIT $${idx}`,
77416
77504
  [...values, limit2]
77417
77505
  );
@@ -77665,6 +77753,7 @@ var WorktreeManager = class {
77665
77753
 
77666
77754
  // src/workers/agent-worker.ts
77667
77755
  var log23 = new Logger("agent-worker");
77756
+ var LEASE_WINDOW_MS = 15 * 6e4;
77668
77757
  var CANONICAL_WORKER_SYSTEM_PROMPT = `You are a Neura worker \u2014 a capable engineering agent executing a task dispatched by the Neura orchestrator. The orchestrator is a voice-first assistant that briefed this task with the user, confirmed intent, and handed it off to you.
77669
77758
 
77670
77759
  Your posture: be decisive. You have full tool access \u2014 Read, Write, Edit, Bash \u2014 scoped to an isolated worktree directory (your cwd). Make progress. Don't ask the user to double-check obvious things. Don't propose a plan and wait for approval when the path is clear.
@@ -77689,12 +77778,13 @@ Actions inside your worktree (creating new files, editing files you created, run
77689
77778
  Communication protocol \u2014 use these tools to report back, not prose:
77690
77779
 
77691
77780
  - \`report_progress(message)\` \u2014 brief status updates. Surfaces to the user as ambient voice. Use sparingly: one update per meaningful step, not one per tool call.
77692
- - \`heartbeat(note?)\` \u2014 signal you're alive on long tasks. Emit at least every 2 minutes when you expect to run longer than that, or the orchestrator will treat you as crashed.
77693
77781
  - \`request_clarification(question, context?, urgency?)\` \u2014 ask the user a blocking question. Returns their answer. Only escalate when you genuinely cannot resolve ambiguity from the task context. Try to answer from context first.
77694
77782
  - \`request_approval(action, rationale?, urgency?)\` \u2014 mandatory before destructive actions (see reversibility rule above).
77695
77783
  - \`complete_task(summary)\` \u2014 mark the task done. Include a short summary of what you did, keyed to the acceptance criteria. The invariant layer will reject this if any clarification or approval is still unresolved.
77696
77784
  - \`fail_task(reason, reason_code)\` \u2014 mark the task failed. Use the right reason_code: \`impossible\` (missing precondition), \`already_done\` (no-op), \`user_aborted\` (user stopped you), \`hard_error\` (exception/timeout).
77697
77785
 
77786
+ You do not need to emit keepalives or heartbeats. The runtime observes your activity through the tool-call stream and refreshes your lease automatically.
77787
+
77698
77788
  Escalation discipline: escalate sparingly. The orchestrator is mediating a voice conversation with a human \u2014 every clarification interrupts it. Only escalate when:
77699
77789
  - You cannot determine which of several paths the user wants (and context doesn't make it obvious).
77700
77790
  - You hit a blocker that requires user authorization (destructive action, external side effect).
@@ -77920,6 +78010,16 @@ var AgentWorker = class {
77920
78010
  callbacks.onStatusChange?.(status);
77921
78011
  },
77922
78012
  onProgress: callbacks.onProgress,
78013
+ onAlive: () => {
78014
+ void this.refreshTaskLease(task.id).catch((err) => {
78015
+ log23.warn("failed to refresh task lease", {
78016
+ workerId,
78017
+ taskId: task.id,
78018
+ err: String(err)
78019
+ });
78020
+ });
78021
+ callbacks.onAlive?.();
78022
+ },
77923
78023
  onComplete: (result) => {
77924
78024
  void this.persistTerminalResult(workerId, result).catch((err) => {
77925
78025
  log23.warn("failed to persist terminal result", {
@@ -77960,6 +78060,10 @@ var AgentWorker = class {
77960
78060
  if (!row.sessionFile) {
77961
78061
  throw new Error(`resume: worker ${workerId} has no session_file`);
77962
78062
  }
78063
+ const linkedTask = await getWorkItemByWorkerId(this.db, workerId);
78064
+ if (linkedTask) {
78065
+ this.workerTaskIds.set(workerId, linkedTask.id);
78066
+ }
77963
78067
  const wrapped = {
77964
78068
  onStatusChange: (status) => {
77965
78069
  void updateWorker(this.db, workerId, { status }).catch((err) => {
@@ -77968,6 +78072,16 @@ var AgentWorker = class {
77968
78072
  callbacks.onStatusChange?.(status);
77969
78073
  },
77970
78074
  onProgress: callbacks.onProgress,
78075
+ onAlive: linkedTask ? () => {
78076
+ void this.refreshTaskLease(linkedTask.id).catch((err) => {
78077
+ log23.warn("failed to refresh task lease on resume", {
78078
+ workerId,
78079
+ taskId: linkedTask.id,
78080
+ err: String(err)
78081
+ });
78082
+ });
78083
+ callbacks.onAlive?.();
78084
+ } : callbacks.onAlive,
77971
78085
  onComplete: (result) => {
77972
78086
  void this.persistTerminalResult(workerId, result).catch((err) => {
77973
78087
  log23.warn("failed to persist terminal result", {
@@ -78077,6 +78191,29 @@ var AgentWorker = class {
78077
78191
  get activeCount() {
78078
78192
  return this.cancellation.activeCount;
78079
78193
  }
78194
+ /**
78195
+ * Refresh a task's lease. Called from the `onAlive` callback the
78196
+ * runtime fires (throttled) on every pi event. Replaces the explicit
78197
+ * `heartbeat` tool the worker used to emit — pi's event stream is a
78198
+ * stronger signal than a self-reported ping and removes a verb from
78199
+ * the worker protocol prompt.
78200
+ *
78201
+ * Terminal tasks are skipped. Pi emits trailing events (agent_end,
78202
+ * final tool_execution_end) after `complete_task`/`fail_task`; without
78203
+ * this guard the lease bump would rewrite `leaseExpiresAt` on a
78204
+ * `done`/`failed`/`cancelled` row and inflate `version`/`updated_at`,
78205
+ * causing spurious optimistic-lock conflicts for the orchestrator's
78206
+ * next edit.
78207
+ */
78208
+ async refreshTaskLease(taskId) {
78209
+ const task = await getWorkItem(this.db, taskId);
78210
+ if (!task) return;
78211
+ if (task.status === "done" || task.status === "failed" || task.status === "cancelled") {
78212
+ return;
78213
+ }
78214
+ const newLease = new Date(Date.now() + LEASE_WINDOW_MS).toISOString();
78215
+ await updateWorkItem(this.db, taskId, { leaseExpiresAt: newLease });
78216
+ }
78080
78217
  /**
78081
78218
  * Persist a terminal result to the workers table. Called by the
78082
78219
  * callbacks wrapper on `onComplete`. Maps the WorkerResult into the
@@ -78201,8 +78338,10 @@ var ClarificationBridge = class {
78201
78338
  if (signal) {
78202
78339
  const onAbort = () => {
78203
78340
  const idx = this.pending.indexOf(pending);
78204
- if (idx >= 0) this.pending.splice(idx, 1);
78205
- reject(new Error("clarification aborted"));
78341
+ if (idx >= 0) {
78342
+ this.pending.splice(idx, 1);
78343
+ reject(new Error("clarification aborted"));
78344
+ }
78206
78345
  };
78207
78346
  if (signal.aborted) {
78208
78347
  onAbort();
@@ -78234,9 +78373,13 @@ var ClarificationBridge = class {
78234
78373
  * clarifications are waiting, the turn is ignored (the user is just
78235
78374
  * talking to Grok normally).
78236
78375
  *
78237
- * Returns true if the turn was consumed by a pending clarification,
78238
- * false otherwise callers can use this to decide whether to also
78239
- * forward the turn to the normal voice session flow.
78376
+ * Returns `true` synchronously when a pending clarification was
78377
+ * found; `false` when nothing was pending. When `true`, the
78378
+ * response persistence (via `onAnswer`) is in flight and the
78379
+ * worker's `askUser` Promise resolves only after the persistence
78380
+ * completes — so `complete_task`'s open-request gate sees the
78381
+ * committed response comment. Callers use the boolean to decide
78382
+ * whether to forward the turn to the normal voice session flow.
78240
78383
  */
78241
78384
  notifyUserTurn(text) {
78242
78385
  const next = this.pending.shift();
@@ -78246,14 +78389,21 @@ var ClarificationBridge = class {
78246
78389
  textPreview: text.slice(0, 80)
78247
78390
  });
78248
78391
  if (next.onAnswer) {
78249
- void Promise.resolve(next.onAnswer(text)).catch((err) => {
78250
- log24.warn("onAnswer persistence hook threw", {
78251
- workerId: next.workerId,
78252
- err: String(err)
78253
- });
78254
- });
78392
+ void (async () => {
78393
+ try {
78394
+ await Promise.resolve(next.onAnswer(text));
78395
+ } catch (err) {
78396
+ log24.warn("onAnswer persistence hook threw", {
78397
+ workerId: next.workerId,
78398
+ err: String(err)
78399
+ });
78400
+ } finally {
78401
+ next.resolve(text);
78402
+ }
78403
+ })();
78404
+ } else {
78405
+ next.resolve(text);
78255
78406
  }
78256
- next.resolve(text);
78257
78407
  return true;
78258
78408
  }
78259
78409
  /** How many clarifications are currently waiting for a user turn. */
@@ -78334,7 +78484,6 @@ var ORCHESTRATOR_ALLOWED_FROM = {
78334
78484
  };
78335
78485
  var WORKER_ALLOWED_COMMENT_TYPES = /* @__PURE__ */ new Set([
78336
78486
  "progress",
78337
- "heartbeat",
78338
78487
  "clarification_request",
78339
78488
  "approval_request",
78340
78489
  "error",
@@ -78472,17 +78621,9 @@ async function resolveTask(store, idOrTitle) {
78472
78621
 
78473
78622
  // src/workers/worker-protocol-tools.ts
78474
78623
  var log25 = new Logger("worker-protocol-tools");
78475
- var LEASE_WINDOW_MS = 5 * 6e4;
78476
78624
  var ReportProgressParams = Type.Object({
78477
78625
  message: Type.String({ description: "Short status update the user may hear read aloud." })
78478
78626
  });
78479
- var HeartbeatParams = Type.Object({
78480
- note: Type.Optional(
78481
- Type.String({
78482
- description: "Optional short note. Heartbeats are pruned after your next real comment."
78483
- })
78484
- )
78485
- });
78486
78627
  var RequestClarificationParams2 = Type.Object({
78487
78628
  question: Type.String({ description: "Plain-language question to ask the user." }),
78488
78629
  context: Type.Optional(
@@ -78576,26 +78717,6 @@ function buildWorkerProtocolTools(options) {
78576
78717
  });
78577
78718
  }
78578
78719
  });
78579
- tools.push({
78580
- name: "heartbeat",
78581
- label: "Heartbeat",
78582
- description: "Signal that you're still alive on a long-running task. Refreshes the worker's lease. Emit at least every 2 minutes when you expect to run long so the orchestrator doesn't treat you as crashed.",
78583
- parameters: HeartbeatParams,
78584
- execute: async (_toolCallId, rawParams) => {
78585
- const params = rawParams;
78586
- const newLease = new Date(Date.now() + LEASE_WINDOW_MS).toISOString();
78587
- const result = await taskTools.updateTask(taskId, {
78588
- comment: { type: "heartbeat", content: params.note ?? "still working" },
78589
- fields: { leaseExpiresAt: newLease }
78590
- });
78591
- if (!result) throw new Error(`heartbeat: task ${taskId} not found`);
78592
- return textResult("Heartbeat recorded.", {
78593
- taskId,
78594
- workerId,
78595
- leaseExpiresAt: newLease
78596
- });
78597
- }
78598
- });
78599
78720
  tools.push({
78600
78721
  name: "request_clarification",
78601
78722
  label: "Request Clarification",
@@ -78964,22 +79085,40 @@ async function handleWorkerControlTool(name, args, ctx) {
78964
79085
  case "pause_worker": {
78965
79086
  const workerId = args.worker_id;
78966
79087
  const result = await ctx.workerControl.pauseWorker(workerId);
78967
- return { result };
79088
+ return {
79089
+ result: { paused: result.paused, ...result.reason ? { reason: result.reason } : {} }
79090
+ };
78968
79091
  }
78969
79092
  case "resume_worker": {
78970
79093
  const workerId = args.worker_id;
78971
79094
  const message = args.message;
78972
79095
  const result = await ctx.workerControl.resumeWorker(workerId, message);
78973
- return { result };
79096
+ return {
79097
+ result: { resumed: result.resumed, ...result.reason ? { reason: result.reason } : {} }
79098
+ };
78974
79099
  }
78975
79100
  case "cancel_worker": {
78976
79101
  const workerId = args.worker_id;
78977
79102
  const result = await ctx.workerControl.cancelWorker(workerId);
78978
- return { result };
79103
+ return {
79104
+ result: {
79105
+ cancelled: result.cancelled,
79106
+ ...result.reason ? { reason: result.reason } : {}
79107
+ }
79108
+ };
78979
79109
  }
78980
79110
  case "list_active_workers": {
78981
79111
  const workers = await ctx.workerControl.listActive();
78982
- return { result: { count: workers.length, workers } };
79112
+ return {
79113
+ result: {
79114
+ count: workers.length,
79115
+ workers: workers.map((w) => ({
79116
+ status: w.status,
79117
+ skillName: w.skillName,
79118
+ startedAt: w.startedAt
79119
+ }))
79120
+ }
79121
+ };
78983
79122
  }
78984
79123
  default:
78985
79124
  return null;
@@ -79288,14 +79427,20 @@ async function initServices() {
79288
79427
  const agentDir = join4(config2.neuraHome, "agent");
79289
79428
  const sessionDir = defaultSessionDir(agentDir);
79290
79429
  const globalSkillsDir = join4(homedir5(), ".neura", "skills");
79430
+ const bundleDir = dirname2(fileURLToPath(import.meta.url));
79431
+ const bundledSkillsDir = join4(bundleDir, "..", "skills");
79291
79432
  skillRegistry = new SkillRegistry();
79292
79433
  skillWatcher = new SkillWatcher({
79293
79434
  registry: skillRegistry,
79294
79435
  cwd: process.cwd(),
79295
- globalSkillsDir
79436
+ globalSkillsDir,
79437
+ bundledSkillsDir
79296
79438
  });
79297
79439
  await skillWatcher.start();
79298
- log29.info("skill registry loaded", { count: skillRegistry.size });
79440
+ log29.info("skill registry loaded", {
79441
+ count: skillRegistry.size,
79442
+ bundledSkillsDir
79443
+ });
79299
79444
  voiceFanoutBridge = new VoiceFanoutBridge({
79300
79445
  interjector: { interject: () => Promise.resolve() }
79301
79446
  });
@@ -79347,7 +79492,12 @@ async function initServices() {
79347
79492
  },
79348
79493
  getTask: (idOrTitle) => store.getWorkItem(idOrTitle),
79349
79494
  listTaskComments: async (taskId, options) => {
79350
- return listComments(rawDb, { taskId, limit: options?.limit });
79495
+ return listComments(rawDb, {
79496
+ taskId,
79497
+ limit: options?.limit,
79498
+ order: options?.order,
79499
+ excludeTypes: options?.excludeTypes
79500
+ });
79351
79501
  },
79352
79502
  updateTask: async (idOrTitle, payload) => {
79353
79503
  const current = await store.getWorkItem(idOrTitle);
@@ -99061,7 +99211,12 @@ function attachWebSocket(httpServer2, services2) {
99061
99211
  listTaskComments: async (taskId, options) => {
99062
99212
  const db = store.getRawDb?.();
99063
99213
  if (!db) throw new Error("store does not expose a raw PGlite handle");
99064
- return listComments(db, { taskId, limit: options?.limit });
99214
+ return listComments(db, {
99215
+ taskId,
99216
+ limit: options?.limit,
99217
+ order: options?.order,
99218
+ excludeTypes: options?.excludeTypes
99219
+ });
99065
99220
  },
99066
99221
  updateTask: async (idOrTitle, payload) => {
99067
99222
  const current = await resolveTask(store, idOrTitle);