@themoltnet/agent-daemon 0.3.0 → 0.4.0
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/main.js +93 -7
- package/package.json +7 -7
package/dist/main.js
CHANGED
|
@@ -6324,6 +6324,25 @@ function buildPromptForTask(task, ctx) {
|
|
|
6324
6324
|
//#endregion
|
|
6325
6325
|
//#region ../../libs/agent-runtime/src/reporters/api.ts
|
|
6326
6326
|
/**
|
|
6327
|
+
* True when an error looks like the Keto "tuple lagged the read API"
|
|
6328
|
+
* race: HTTP 403 plus a server message starting with "Not authorized".
|
|
6329
|
+
* Used to gate the first-append retry to the narrow recoverable case.
|
|
6330
|
+
*
|
|
6331
|
+
* The server's task.service.ts returns errors of the form
|
|
6332
|
+
* `Not authorized to <verb> ...`
|
|
6333
|
+
* for every claimant-relation check (append messages, list messages,
|
|
6334
|
+
* heartbeat, complete, fail). Other Fastify 403s — auth-plugin
|
|
6335
|
+
* rejection, route-level guards — use different message shapes and
|
|
6336
|
+
* are not consistency-window flakes; retrying them just delays the
|
|
6337
|
+
* permanent failure surfacing.
|
|
6338
|
+
*/
|
|
6339
|
+
function isKetoConsistencyLag403(err) {
|
|
6340
|
+
if (!err || typeof err !== "object") return false;
|
|
6341
|
+
if (err.statusCode !== 403) return false;
|
|
6342
|
+
const message = err.message ?? "";
|
|
6343
|
+
return /Not authorized/i.test(message);
|
|
6344
|
+
}
|
|
6345
|
+
/**
|
|
6327
6346
|
* TaskReporter backed by the Tasks API via the SDK's TasksNamespace.
|
|
6328
6347
|
*
|
|
6329
6348
|
* - `open()` fires an immediate heartbeat (satisfies DBOS recv('started', 300s))
|
|
@@ -6349,6 +6368,15 @@ var ApiTaskReporter = class {
|
|
|
6349
6368
|
* failures are never silently dropped by the batching layer.
|
|
6350
6369
|
*/
|
|
6351
6370
|
pendingError = null;
|
|
6371
|
+
/**
|
|
6372
|
+
* Flips to `true` after the first successful `appendMessages` call.
|
|
6373
|
+
* Used to gate the 403-retry window in `flush` to the very first
|
|
6374
|
+
* append (the only point where the Keto `Task:claimant#Agent` tuple
|
|
6375
|
+
* can lag the read API). Once any append has succeeded, Keto is
|
|
6376
|
+
* known consistent for this task and a 403 thereafter is a real
|
|
6377
|
+
* authorization failure that should surface immediately.
|
|
6378
|
+
*/
|
|
6379
|
+
firstAppendSucceeded = false;
|
|
6352
6380
|
cancelController = new AbortController();
|
|
6353
6381
|
observedCancelReason = null;
|
|
6354
6382
|
maxBatchSize;
|
|
@@ -6376,6 +6404,7 @@ var ApiTaskReporter = class {
|
|
|
6376
6404
|
}
|
|
6377
6405
|
this.taskId = ctx.taskId;
|
|
6378
6406
|
this.attemptN = ctx.attemptN;
|
|
6407
|
+
this.firstAppendSucceeded = false;
|
|
6379
6408
|
await this.sendInitialHeartbeat();
|
|
6380
6409
|
const intervalMs = this.opts.heartbeatIntervalMs ?? 6e4;
|
|
6381
6410
|
if (intervalMs > 0) this.heartbeatTimer = setInterval(() => {
|
|
@@ -6421,7 +6450,8 @@ var ApiTaskReporter = class {
|
|
|
6421
6450
|
const batch = this.buffer.splice(0, this.buffer.length);
|
|
6422
6451
|
this.inFlight = (async () => {
|
|
6423
6452
|
try {
|
|
6424
|
-
await this.
|
|
6453
|
+
await this.appendWithFirstCallRetry(batch);
|
|
6454
|
+
this.firstAppendSucceeded = true;
|
|
6425
6455
|
} catch (err) {
|
|
6426
6456
|
const overflowCap = this.maxBatchSize * 3;
|
|
6427
6457
|
const restoredCount = batch.length;
|
|
@@ -6498,18 +6528,53 @@ var ApiTaskReporter = class {
|
|
|
6498
6528
|
async sendInitialHeartbeat() {
|
|
6499
6529
|
const maxAttempts = 5;
|
|
6500
6530
|
const baseDelayMs = 100;
|
|
6501
|
-
let lastErr;
|
|
6502
6531
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
|
|
6503
6532
|
await this.sendHeartbeat();
|
|
6504
6533
|
return;
|
|
6505
6534
|
} catch (err) {
|
|
6506
|
-
lastErr = err;
|
|
6507
6535
|
if ((err && typeof err === "object" && "statusCode" in err ? err.statusCode : void 0) !== 403 || attempt === maxAttempts) throw err;
|
|
6508
6536
|
await new Promise((resolve) => {
|
|
6509
6537
|
setTimeout(resolve, baseDelayMs * attempt);
|
|
6510
6538
|
});
|
|
6511
6539
|
}
|
|
6512
|
-
|
|
6540
|
+
}
|
|
6541
|
+
/**
|
|
6542
|
+
* Wrap `tasks.appendMessages` with a bounded 403-retry on the first
|
|
6543
|
+
* call only. Mirrors `sendInitialHeartbeat` — the same Keto
|
|
6544
|
+
* `Task:claimant#Agent` tuple race covers both endpoints, and a
|
|
6545
|
+
* batched-up first flush triggered immediately after `open()` (e.g.
|
|
6546
|
+
* a fast executor that records a `task_started` info message before
|
|
6547
|
+
* any heartbeat round-trip completes) can hit a 403 even though the
|
|
6548
|
+
* heartbeat itself eventually succeeds.
|
|
6549
|
+
*
|
|
6550
|
+
* Once `firstAppendSucceeded` is set we know Keto is consistent for
|
|
6551
|
+
* this task and any subsequent 403 is a real authorization failure
|
|
6552
|
+
* (e.g. another agent stole the claim) that must surface immediately.
|
|
6553
|
+
*
|
|
6554
|
+
* Retry budget is intentionally tight: the documented Keto
|
|
6555
|
+
* consistency window is "tens of milliseconds," so 100 ms + 200 ms
|
|
6556
|
+
* (3 attempts total) covers the realistic window with margin without
|
|
6557
|
+
* adding meaningful latency to the permanent-failure path (wrong
|
|
6558
|
+
* task ID, claim revoked, etc.). We also string-match the server's
|
|
6559
|
+
* `Not authorized` message to avoid retrying 403s that mean
|
|
6560
|
+
* something else (Fastify routes can 403 for non-auth reasons too).
|
|
6561
|
+
*/
|
|
6562
|
+
async appendWithFirstCallRetry(batch) {
|
|
6563
|
+
if (this.firstAppendSucceeded) {
|
|
6564
|
+
await this.opts.tasks.appendMessages(this.taskId, this.attemptN, { messages: batch });
|
|
6565
|
+
return;
|
|
6566
|
+
}
|
|
6567
|
+
const maxAttempts = 3;
|
|
6568
|
+
const baseDelayMs = 100;
|
|
6569
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
|
|
6570
|
+
await this.opts.tasks.appendMessages(this.taskId, this.attemptN, { messages: batch });
|
|
6571
|
+
return;
|
|
6572
|
+
} catch (err) {
|
|
6573
|
+
if (attempt === maxAttempts || !isKetoConsistencyLag403(err)) throw err;
|
|
6574
|
+
await new Promise((resolve) => {
|
|
6575
|
+
setTimeout(resolve, baseDelayMs * attempt);
|
|
6576
|
+
});
|
|
6577
|
+
}
|
|
6513
6578
|
}
|
|
6514
6579
|
async sendHeartbeat() {
|
|
6515
6580
|
const body = this.opts.leaseTtlSec ? { leaseTtlSec: this.opts.leaseTtlSec } : {};
|
|
@@ -14792,7 +14857,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
14792
14857
|
correlationId: task.correlationId ?? null
|
|
14793
14858
|
})
|
|
14794
14859
|
});
|
|
14795
|
-
const piAuthDir = join(homedir(), ".pi", "agent");
|
|
14860
|
+
const piAuthDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
14796
14861
|
const modelHandle = getModel(opts.provider, opts.model);
|
|
14797
14862
|
const piOtelExtension = createPiOtelExtension({
|
|
14798
14863
|
agentName: opts.agentName,
|
|
@@ -15607,6 +15672,26 @@ async function runOnce(argv) {
|
|
|
15607
15672
|
sandbox: sandbox.path,
|
|
15608
15673
|
taskId
|
|
15609
15674
|
}, "agent-daemon.starting");
|
|
15675
|
+
let runtime = null;
|
|
15676
|
+
const onSignal = (sig) => {
|
|
15677
|
+
rootLogger.warn({
|
|
15678
|
+
signal: sig,
|
|
15679
|
+
taskId
|
|
15680
|
+
}, "agent-daemon.draining");
|
|
15681
|
+
runtime?.stop();
|
|
15682
|
+
ctx.agent.tasks.cancel(taskId, { reason: `runner_${sig.toLowerCase()}` }).catch((err) => {
|
|
15683
|
+
rootLogger.warn({
|
|
15684
|
+
err: err instanceof Error ? err.message : String(err),
|
|
15685
|
+
taskId
|
|
15686
|
+
}, "agent-daemon.cancel_on_signal_failed");
|
|
15687
|
+
});
|
|
15688
|
+
};
|
|
15689
|
+
process.on("SIGINT", () => {
|
|
15690
|
+
onSignal("SIGINT");
|
|
15691
|
+
});
|
|
15692
|
+
process.on("SIGTERM", () => {
|
|
15693
|
+
onSignal("SIGTERM");
|
|
15694
|
+
});
|
|
15610
15695
|
try {
|
|
15611
15696
|
const executeTask = createPiTaskExecutor({
|
|
15612
15697
|
agentName: opts.agent,
|
|
@@ -15619,7 +15704,7 @@ async function runOnce(argv) {
|
|
|
15619
15704
|
gh: createGhCliClient(),
|
|
15620
15705
|
logger: rootLogger
|
|
15621
15706
|
});
|
|
15622
|
-
|
|
15707
|
+
runtime = new AgentRuntime({
|
|
15623
15708
|
logger: rootLogger,
|
|
15624
15709
|
source: new ApiTaskSource({
|
|
15625
15710
|
agent: ctx.agent,
|
|
@@ -15639,7 +15724,8 @@ async function runOnce(argv) {
|
|
|
15639
15724
|
log: (msg, err) => rootLogger.warn({ err }, msg)
|
|
15640
15725
|
}),
|
|
15641
15726
|
executeTask
|
|
15642
|
-
})
|
|
15727
|
+
});
|
|
15728
|
+
const [output] = await runtime.start();
|
|
15643
15729
|
if (!output) {
|
|
15644
15730
|
rootLogger.error({}, "agent-daemon.no_output");
|
|
15645
15731
|
return 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=22"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@earendil-works/gondolin": "^0.
|
|
25
|
+
"@earendil-works/gondolin": "^0.9.1",
|
|
26
26
|
"@earendil-works/pi-ai": "^0.74.0",
|
|
27
27
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
28
28
|
"@opentelemetry/api": "^1.9.0",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
34
34
|
"pino": "^10.3.1",
|
|
35
35
|
"pino-pretty": "^13.1.3",
|
|
36
|
-
"@themoltnet/agent-runtime": "0.
|
|
37
|
-
"@themoltnet/
|
|
38
|
-
"@themoltnet/
|
|
36
|
+
"@themoltnet/agent-runtime": "0.11.0",
|
|
37
|
+
"@themoltnet/sdk": "0.99.0",
|
|
38
|
+
"@themoltnet/pi-extension": "0.13.5"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsx": "^4.7.0",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"vite": "^8.0.0",
|
|
44
44
|
"vitest": "^3.0.0",
|
|
45
45
|
"@moltnet/bootstrap": "0.1.0",
|
|
46
|
-
"@moltnet/crypto-service": "0.1.0",
|
|
47
46
|
"@moltnet/database": "0.1.0",
|
|
48
|
-
"@moltnet/tasks": "0.1.0"
|
|
47
|
+
"@moltnet/tasks": "0.1.0",
|
|
48
|
+
"@moltnet/crypto-service": "0.1.0"
|
|
49
49
|
},
|
|
50
50
|
"nx": {
|
|
51
51
|
"tags": [
|