@themoltnet/agent-daemon 0.5.3 → 0.6.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.
Files changed (2) hide show
  1. package/dist/main.js +200 -59
  2. package/package.json +5 -5
package/dist/main.js CHANGED
@@ -3,7 +3,7 @@ import crypto, { createHash } from "crypto";
3
3
  import { parseArgs, parseEnv, promisify } from "node:util";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
5
5
  import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
6
- import { pino } from "pino";
6
+ import { pino, transport } from "pino";
7
7
  import { readFile } from "node:fs/promises";
8
8
  import { createHash as createHash$1 } from "node:crypto";
9
9
  import path, { dirname, isAbsolute, join, resolve } from "node:path";
@@ -12,6 +12,7 @@ import { execFile, execFileSync } from "node:child_process";
12
12
  import { DefaultResourceLoader, SessionManager, createAgentSession, createBashToolDefinition, createEditToolDefinition, createReadToolDefinition, createSyntheticSourceInfo, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
13
13
  import { Type, getModel } from "@earendil-works/pi-ai";
14
14
  import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
15
+ import { once } from "node:events";
15
16
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
16
17
  import { resourceFromAttributes } from "@opentelemetry/resources";
17
18
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
@@ -5990,6 +5991,14 @@ var COMMON_OPTIONAL_FLAGS = `\
5990
5991
  --heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
5991
5992
  --max-batch-size <n> Reporter message batch size. Default: 50.
5992
5993
  --flush-interval-ms <n> Reporter flush window. Default: 200.
5994
+ --max-turns <n> Cap on tool-use turns per attempt; over this,
5995
+ the pi session aborts and the attempt fails
5996
+ with code 'max_turns_exceeded'. 0 = disabled.
5997
+ Default: 0. Recommended ~30 for fulfill_brief.
5998
+ --max-bash-timeouts <n> Cap on bash-tool timeouts per attempt; over
5999
+ this, the pi session aborts and the attempt
6000
+ fails with code 'max_bash_timeouts_exceeded'.
6001
+ 0 = disabled. Default: 3.
5993
6002
  --debug Verbose logging: also log successful list/claim
5994
6003
  outcomes (candidate counts, claim attempts).`;
5995
6004
  var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
@@ -14344,7 +14353,7 @@ function createMoltNetTools(config) {
14344
14353
  const createEntry = defineTool({
14345
14354
  name: "moltnet_create_entry",
14346
14355
  label: "Create MoltNet Diary Entry",
14347
- description: "Create a new diary entry to record decisions, findings, incidents, or reflections. During an active task, the entry is forced into the task diary and tagged with the task:* provenance namespace (task:id:<id>, task:type:<type>, task:attempt:<n>, plus task:correlation:<id> when set); an explicit diaryId mismatching the task diary is rejected.",
14356
+ description: "Create a new diary entry to record decisions, findings, incidents, or reflections. During an active task, the entry is forced into the task diary and tagged with the task:* provenance namespace (task:id:<id>, task:type:<type>, task:attempt:<n>, plus task:correlation:<id> when set); an explicit diaryId mismatching the task diary is rejected. Use this tool — NOT `moltnet entry create` / `moltnet entry create-signed` via bash. The CLI path bypasses task-tag auto-injection and leaves entries invisible to taskFilter queries.",
14348
14357
  parameters: Type.Object({
14349
14358
  title: Type.String({ description: "Entry title (concise, descriptive)" }),
14350
14359
  content: Type.String({ description: "Entry content (markdown)" }),
@@ -14924,38 +14933,48 @@ async function resumeVm(config) {
14924
14933
  [GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
14925
14934
  } }
14926
14935
  });
14927
- await vm.exec(`sh -c '
14936
+ try {
14937
+ await vm.exec(`sh -c '
14928
14938
  cp /etc/gondolin/mitm/ca.crt /usr/local/share/ca-certificates/gondolin-mitm.crt
14929
14939
  update-ca-certificates 2>/dev/null
14930
14940
  cat /etc/gondolin/mitm/ca.crt >> /etc/ssl/certs/ca-certificates.crt
14931
14941
  '`);
14932
- await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`);
14933
- await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`);
14934
- for (const [i, cmd] of (config.sandboxConfig?.resumeCommands ?? []).entries()) await vmRun(vm, `resumeCommands[${i}]`, cmd);
14935
- const vmSshDir = `${vmAgentDir}/ssh`;
14936
- await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
14937
- if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
14938
- const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
14939
- await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
14940
- await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
14941
- if (creds.gitconfig) {
14942
- const vmSigningKey = `${vmSshDir}/id_ed25519`;
14943
- let vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
14944
- vmGitconfig = ensureRelativeWorktreePaths(vmGitconfig);
14945
- await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, { mode: 420 });
14946
- }
14947
- if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
14948
- if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
14949
- if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
14950
- if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
14951
- await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
14952
- return {
14953
- vm,
14954
- credentials: creds,
14955
- mountPath: config.mountPath,
14956
- guestWorkspace: GUEST_WORKSPACE$1,
14957
- agentDir
14958
- };
14942
+ await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`);
14943
+ await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`);
14944
+ for (const [i, cmd] of (config.sandboxConfig?.resumeCommands ?? []).entries()) await vmRun(vm, `resumeCommands[${i}]`, cmd);
14945
+ const vmSshDir = `${vmAgentDir}/ssh`;
14946
+ await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
14947
+ if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
14948
+ const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
14949
+ await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
14950
+ await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
14951
+ if (creds.gitconfig) {
14952
+ const vmSigningKey = `${vmSshDir}/id_ed25519`;
14953
+ let vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
14954
+ vmGitconfig = ensureRelativeWorktreePaths(vmGitconfig);
14955
+ await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, { mode: 420 });
14956
+ }
14957
+ if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
14958
+ if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
14959
+ if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
14960
+ if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
14961
+ await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
14962
+ return {
14963
+ vm,
14964
+ credentials: creds,
14965
+ mountPath: config.mountPath,
14966
+ guestWorkspace: GUEST_WORKSPACE$1,
14967
+ agentDir
14968
+ };
14969
+ } catch (err) {
14970
+ try {
14971
+ await vm.close();
14972
+ } catch (closeErr) {
14973
+ const m = closeErr instanceof Error ? closeErr.message : String(closeErr);
14974
+ process.stderr.write(`[vm] post-throw vm.close() failed: ${m}\n`);
14975
+ }
14976
+ throw err;
14977
+ }
14959
14978
  }
14960
14979
  /**
14961
14980
  * Rewrite host-absolute paths inside moltnet.json to VM-local equivalents.
@@ -15464,8 +15483,8 @@ function buildRuntimeInstructor(ctx) {
15464
15483
  "## Diary discipline",
15465
15484
  "",
15466
15485
  `- During this task, every diary entry MUST land in \`${ctx.diaryId}\``,
15467
- " (the task diary). The MCP `moltnet_create_entry` tool enforces this",
15468
- " and rejects mismatched explicit `diaryId` parameters.",
15486
+ " (the task diary). The `moltnet_create_entry` custom tool enforces",
15487
+ " this and rejects mismatched explicit `diaryId` parameters.",
15469
15488
  `- Provenance tags \`task:id:${ctx.taskId}\`, \`task:type:${ctx.taskType}\`,`,
15470
15489
  ` and \`task:attempt:${ctx.attemptN}\`${ctx.correlationId ? `, plus \`task:correlation:${ctx.correlationId}\`` : ""} are auto-injected on every entry.`,
15471
15490
  " These share the `task:` namespace so `moltnet_diary_tags` with",
@@ -15473,12 +15492,23 @@ function buildRuntimeInstructor(ctx) {
15473
15492
  " `taskFilter` shorthand on `moltnet_list_entries` /",
15474
15493
  " `moltnet_search_entries` expands into them. You may add additional",
15475
15494
  " tags but you cannot remove the auto-injected ones.",
15495
+ "- **DO NOT shell out to `moltnet entry create` / `moltnet entry",
15496
+ " create-signed` / any other `moltnet entry` subcommand via bash.**",
15497
+ " Those CLI paths hit the REST API directly and bypass the",
15498
+ " custom tool's task-tag auto-injection, leaving you with",
15499
+ " untagged entries that `moltnet_list_entries` with a",
15500
+ " `taskFilter: { taskId: ... }` cannot find. The legreffier skill",
15501
+ " recommends `moltnet entry *` for normal interactive sessions —",
15502
+ " inside a running task that advice does not apply. Use the",
15503
+ " `moltnet_create_entry` custom tool only.",
15476
15504
  "",
15477
15505
  "## Accountable commits",
15478
15506
  "",
15479
15507
  "- Every commit you make during this task MUST be paired with a signed",
15480
- " diary entry created via `moltnet_create_entry`. Embed the returned",
15481
- " entry id in the commit trailer `MoltNet-Diary: <id>`.",
15508
+ " diary entry created via the `moltnet_create_entry` custom tool",
15509
+ " (NOT via `moltnet entry create-signed` from bash — see Diary",
15510
+ " discipline above). Embed the returned entry id in the commit",
15511
+ " trailer `MoltNet-Diary: <id>`.",
15482
15512
  "- Commits must be signed with the agent credentials (gitconfig is",
15483
15513
  " pre-configured). Do not bypass signing.",
15484
15514
  "",
@@ -16175,6 +16205,11 @@ async function executePiTask(claimedTask, reporter, opts) {
16175
16205
  let assistantText = "";
16176
16206
  let reporterError = null;
16177
16207
  const usage = finalUsage;
16208
+ let capAbort = null;
16209
+ let toolUseTurnCount = 0;
16210
+ let bashTimeoutCount = 0;
16211
+ const maxTurns = opts.maxTurns ?? 0;
16212
+ const maxBashTimeouts = opts.maxBashTimeouts ?? 3;
16178
16213
  cancelListener = wireSessionAbort(reporter.cancelSignal, session);
16179
16214
  const recordingPromise = [];
16180
16215
  const track = (p) => {
@@ -16189,6 +16224,23 @@ async function executePiTask(claimedTask, reporter, opts) {
16189
16224
  }
16190
16225
  }));
16191
16226
  };
16227
+ const liveSession = session;
16228
+ const triggerCapAbort = (code, message) => {
16229
+ if (capAbort) return;
16230
+ capAbort = {
16231
+ code,
16232
+ message
16233
+ };
16234
+ liveSession.abort().catch((err) => {
16235
+ const m = err instanceof Error ? err.message : String(err);
16236
+ process.stderr.write(`[cap] session.abort() failed: ${m}\n`);
16237
+ });
16238
+ track(emit("info", {
16239
+ event: "cap_abort",
16240
+ code,
16241
+ message
16242
+ }));
16243
+ };
16192
16244
  session.subscribe((event) => {
16193
16245
  if (event.type === "message_update") {
16194
16246
  const ae = event.assistantMessageEvent;
@@ -16197,12 +16249,17 @@ async function executePiTask(claimedTask, reporter, opts) {
16197
16249
  track(emit("text_delta", { delta: ae.delta }));
16198
16250
  }
16199
16251
  } else if (event.type === "tool_execution_start") track(emit("tool_call_start", { tool_name: event.toolName }));
16200
- else if (event.type === "tool_execution_end") track(emit("tool_call_end", {
16201
- tool_name: event.toolName,
16202
- is_error: event.isError,
16203
- result: event.isError ? truncateForWire(event.result) : void 0
16204
- }));
16205
- else if (event.type === "turn_end") {
16252
+ else if (event.type === "tool_execution_end") {
16253
+ track(emit("tool_call_end", {
16254
+ tool_name: event.toolName,
16255
+ is_error: event.isError,
16256
+ result: event.isError ? truncateForWire(event.result) : void 0
16257
+ }));
16258
+ if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
16259
+ bashTimeoutCount += 1;
16260
+ if (bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
16261
+ }
16262
+ } else if (event.type === "turn_end") {
16206
16263
  const msg = event.message;
16207
16264
  if (msg?.role === "assistant" && msg.usage) {
16208
16265
  usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
@@ -16212,7 +16269,12 @@ async function executePiTask(claimedTask, reporter, opts) {
16212
16269
  if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
16213
16270
  if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
16214
16271
  }
16215
- track(emit("turn_end", { stop_reason: msg?.stopReason ?? "end_turn" }));
16272
+ const stopReason = msg?.stopReason ?? "end_turn";
16273
+ track(emit("turn_end", { stop_reason: stopReason }));
16274
+ if (maxTurns > 0 && stopReason !== "end_turn" && stopReason !== "aborted" && stopReason !== "error") {
16275
+ toolUseTurnCount += 1;
16276
+ if (toolUseTurnCount >= maxTurns) triggerCapAbort("max_turns_exceeded", `Aborted after ${toolUseTurnCount} tool-use turns (cap ${maxTurns}).`);
16277
+ }
16216
16278
  llmAbort = msg?.stopReason === "error";
16217
16279
  if (msg?.stopReason === "error") llmErrorMessage = typeof msg.errorMessage === "string" && msg.errorMessage.length > 0 ? msg.errorMessage : null;
16218
16280
  else llmErrorMessage = null;
@@ -16241,7 +16303,7 @@ async function executePiTask(claimedTask, reporter, opts) {
16241
16303
  let parsedOutput = null;
16242
16304
  let parsedOutputCid = null;
16243
16305
  let parseError = null;
16244
- if (!runError && !llmAbort && !cancelled) {
16306
+ if (!runError && !llmAbort && !cancelled && !capAbort) {
16245
16307
  const captured = submitToolHandle?.getCaptured() ?? null;
16246
16308
  if (captured) try {
16247
16309
  parsedOutput = captured;
@@ -16294,6 +16356,21 @@ async function executePiTask(claimedTask, reporter, opts) {
16294
16356
  retryable: false
16295
16357
  }
16296
16358
  };
16359
+ const capAbortSnapshot = capAbort;
16360
+ if (capAbortSnapshot) return {
16361
+ taskId: task.id,
16362
+ attemptN,
16363
+ status: "failed",
16364
+ output: null,
16365
+ outputCid: null,
16366
+ usage,
16367
+ durationMs: Date.now() - startTime,
16368
+ error: {
16369
+ code: capAbortSnapshot.code,
16370
+ message: capAbortSnapshot.message,
16371
+ retryable: false
16372
+ }
16373
+ };
16297
16374
  const status = runError || llmAbort || parseError || reporterError ? "failed" : "completed";
16298
16375
  const errorCode = runError?.code ?? parseError?.code ?? reporterError?.code ?? (llmAbort ? "llm_api_error" : void 0);
16299
16376
  const errorMessage = runError?.message ?? parseError?.message ?? reporterError?.message ?? (llmAbort ? llmErrorMessage ?? "LLM API error during turn" : void 0);
@@ -16395,6 +16472,25 @@ function summarizePayloadForLog(kind, payload) {
16395
16472
  default: return payload;
16396
16473
  }
16397
16474
  }
16475
+ /**
16476
+ * Detect pi's bash-timeout error wrapper in a `tool_execution_end`
16477
+ * result. The bash tool surfaces a timeout as a structured tool result
16478
+ * `{ content: [{ type: 'text', text: '… Command timed out after N
16479
+ * seconds' }] }` (see `@earendil-works/pi-coding-agent`'s bash.js).
16480
+ * Substring-match against the stable wrapper string is the only
16481
+ * mechanism short of patching pi; the string is part of pi's external
16482
+ * tool-error API and changing it would break agents that read tool
16483
+ * errors.
16484
+ */
16485
+ function isBashTimeoutResult(result) {
16486
+ if (result === null || result === void 0) return false;
16487
+ if (typeof result === "string") return result.includes("Command timed out after");
16488
+ if (typeof result !== "object") return false;
16489
+ const content = result.content;
16490
+ if (!Array.isArray(content)) return false;
16491
+ for (const part of content) if (typeof part === "object" && part !== null && typeof part.text === "string" && part.text.includes("Command timed out after")) return true;
16492
+ return false;
16493
+ }
16398
16494
  var TRUNCATE_LIMIT = 4 * 1024;
16399
16495
  function truncateForWire(value) {
16400
16496
  if (value === null || value === void 0) return value;
@@ -16552,12 +16648,53 @@ async function maybeWriteAnchors(output, ctx) {
16552
16648
  }
16553
16649
  }
16554
16650
  //#endregion
16651
+ //#region src/lib/logger.ts
16652
+ /**
16653
+ * Logger setup + teardown for the agent-daemon CLI commands.
16654
+ *
16655
+ * Why this helper exists: pino with `pino-pretty` transport spawns a
16656
+ * Node Worker thread for human-readable output. The Worker holds the
16657
+ * parent's TTYWrap handles ref'd; without an explicit `.end()` the
16658
+ * daemon never exits after the task completes — symptom: `once`
16659
+ * prints `[done] TaskOutput: …` then hangs until Ctrl-C
16660
+ * (see issue #1107). This module owns both creation and shutdown
16661
+ * so every CLI command gets the same cleanup contract without
16662
+ * duplicating the dance.
16663
+ *
16664
+ * Non-TTY mode (CI, file redirect) skips the transport entirely:
16665
+ * pino writes raw NDJSON directly to stderr, no worker thread,
16666
+ * nothing extra to clean up.
16667
+ */
16668
+ function createRootLogger(options) {
16669
+ const prettyTransport = process.stderr.isTTY ? transport({
16670
+ target: "pino-pretty",
16671
+ options: { colorize: true }
16672
+ }) : null;
16673
+ const logger = pino(options, prettyTransport ?? void 0);
16674
+ const shutdown = async () => {
16675
+ if (!prettyTransport) return;
16676
+ logger.flush();
16677
+ try {
16678
+ prettyTransport.end();
16679
+ await once(prettyTransport, "close");
16680
+ } catch (err) {
16681
+ process.stderr.write(`[pino] transport teardown failed: ` + (err instanceof Error ? err.message : String(err)) + "\n");
16682
+ }
16683
+ };
16684
+ return {
16685
+ logger,
16686
+ shutdown
16687
+ };
16688
+ }
16689
+ //#endregion
16555
16690
  //#region src/lib/options.ts
16556
16691
  var DEFAULTS = {
16557
16692
  leaseTtlSec: 300,
16558
16693
  heartbeatIntervalMs: 6e4,
16559
16694
  maxBatchSize: 50,
16560
- flushIntervalMs: 200
16695
+ flushIntervalMs: 200,
16696
+ maxTurns: 0,
16697
+ maxBashTimeouts: 3
16561
16698
  };
16562
16699
  var MissingRequiredOptionError = class extends Error {
16563
16700
  constructor(flag) {
@@ -16579,6 +16716,8 @@ function parseCommonOptions(args) {
16579
16716
  heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", DEFAULTS.heartbeatIntervalMs),
16580
16717
  maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", DEFAULTS.maxBatchSize),
16581
16718
  flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
16719
+ maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
16720
+ maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
16582
16721
  debug: args.debug === true
16583
16722
  };
16584
16723
  }
@@ -16612,6 +16751,8 @@ function commonOptionDefs() {
16612
16751
  "heartbeat-interval-ms": { type: "string" },
16613
16752
  "max-batch-size": { type: "string" },
16614
16753
  "flush-interval-ms": { type: "string" },
16754
+ "max-turns": { type: "string" },
16755
+ "max-bash-timeouts": { type: "string" },
16615
16756
  debug: { type: "boolean" }
16616
16757
  };
16617
16758
  }
@@ -16778,14 +16919,11 @@ async function runPolling(opts) {
16778
16919
  "moltnet.llm.model": common.model
16779
16920
  }
16780
16921
  });
16781
- const rootLogger = pino({
16922
+ const { logger, shutdown: shutdownLogger } = createRootLogger({
16782
16923
  name: `agent-daemon.${opts.modeLabel}`,
16783
- level: cfg.logLevel || (common.debug ? "debug" : "info"),
16784
- ...process.stderr.isTTY ? { transport: {
16785
- target: "pino-pretty",
16786
- options: { colorize: true }
16787
- } } : {}
16788
- }).child({
16924
+ level: cfg.logLevel || (common.debug ? "debug" : "info")
16925
+ });
16926
+ const rootLogger = logger.child({
16789
16927
  mode: opts.modeLabel,
16790
16928
  agent: common.agent,
16791
16929
  teamId,
@@ -16818,7 +16956,9 @@ async function runPolling(opts) {
16818
16956
  provider: common.provider,
16819
16957
  model: common.model,
16820
16958
  sandboxConfig: sandbox.config,
16821
- makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger)
16959
+ makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
16960
+ maxTurns: common.maxTurns,
16961
+ maxBashTimeouts: common.maxBashTimeouts
16822
16962
  });
16823
16963
  runtime = new AgentRuntime({
16824
16964
  logger: rootLogger,
@@ -16897,6 +17037,7 @@ async function runPolling(opts) {
16897
17037
  return drained.some((o) => o.status !== "completed") ? 1 : 0;
16898
17038
  } finally {
16899
17039
  await otelShutdown();
17040
+ await shutdownLogger();
16900
17041
  }
16901
17042
  }
16902
17043
  function parseCsv(raw) {
@@ -16968,14 +17109,11 @@ async function runOnce(argv) {
16968
17109
  "moltnet.llm.model": opts.model
16969
17110
  }
16970
17111
  });
16971
- const rootLogger = pino({
17112
+ const { logger, shutdown: shutdownLogger } = createRootLogger({
16972
17113
  name: "agent-daemon.once",
16973
- level: cfg.logLevel || (opts.debug ? "debug" : "info"),
16974
- ...process.stderr.isTTY ? { transport: {
16975
- target: "pino-pretty",
16976
- options: { colorize: true }
16977
- } } : {}
16978
- }).child({
17114
+ level: cfg.logLevel || (opts.debug ? "debug" : "info")
17115
+ });
17116
+ const rootLogger = logger.child({
16979
17117
  mode: "once",
16980
17118
  agent: opts.agent,
16981
17119
  provider: opts.provider,
@@ -17012,7 +17150,9 @@ async function runOnce(argv) {
17012
17150
  provider: opts.provider,
17013
17151
  model: opts.model,
17014
17152
  sandboxConfig: sandbox.config,
17015
- onTurnEvent: makeTurnEventHandler(rootLogger, { taskId })
17153
+ onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
17154
+ maxTurns: opts.maxTurns,
17155
+ maxBashTimeouts: opts.maxBashTimeouts
17016
17156
  });
17017
17157
  const writeCorrelationAnchors = makePrBodyAnchorWriter({
17018
17158
  gh: createGhCliClient(),
@@ -17049,6 +17189,7 @@ async function runOnce(argv) {
17049
17189
  return output.status === "completed" ? 0 : 1;
17050
17190
  } finally {
17051
17191
  await otelShutdown();
17192
+ await shutdownLogger();
17052
17193
  }
17053
17194
  }
17054
17195
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.5.3",
3
+ "version": "0.6.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.",
@@ -35,17 +35,17 @@
35
35
  "pino-pretty": "^13.1.3",
36
36
  "@themoltnet/agent-runtime": "0.15.0",
37
37
  "@themoltnet/sdk": "0.102.0",
38
- "@themoltnet/pi-extension": "0.15.2"
38
+ "@themoltnet/pi-extension": "0.16.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "tsx": "^4.7.0",
42
42
  "typescript": "^5.3.3",
43
43
  "vite": "^8.0.0",
44
44
  "vitest": "^3.0.0",
45
- "@moltnet/crypto-service": "0.1.0",
46
- "@moltnet/database": "0.1.0",
47
45
  "@moltnet/bootstrap": "0.1.0",
48
- "@moltnet/tasks": "0.1.0"
46
+ "@moltnet/crypto-service": "0.1.0",
47
+ "@moltnet/tasks": "0.1.0",
48
+ "@moltnet/database": "0.1.0"
49
49
  },
50
50
  "nx": {
51
51
  "tags": [