@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.
- package/core/server.bundled.mjs +216 -61
- package/core/server.bundled.mjs.map +3 -3
- package/core/version.txt +1 -1
- package/package.json +2 -1
- package/skills/orchestrator-worker-control/SKILL.md +134 -0
- package/skills/red-test-triage/SKILL.md +80 -0
- package/skills/wiki-upload/SKILL.md +79 -0
- package/stores/index.js.map +2 -2
- package/stores/task-comment-queries.d.ts +13 -0
- package/stores/task-comment-queries.d.ts.map +1 -1
- package/stores/task-comment-queries.js +7 -1
- package/stores/task-comment-queries.js.map +1 -1
- package/stores/task-comment-queries.test.js +50 -0
- package/stores/task-comment-queries.test.js.map +1 -1
- package/stores/work-item-queries.d.ts +7 -0
- package/stores/work-item-queries.d.ts.map +1 -1
- package/stores/work-item-queries.js +10 -0
- package/stores/work-item-queries.js.map +1 -1
package/core/server.bundled.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 /
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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)
|
|
78205
|
-
|
|
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
|
|
78238
|
-
* false
|
|
78239
|
-
*
|
|
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
|
|
78250
|
-
|
|
78251
|
-
|
|
78252
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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", {
|
|
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, {
|
|
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, {
|
|
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);
|