@themoltnet/agent-daemon 0.17.0 → 0.18.1

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 (3) hide show
  1. package/README.md +19 -8
  2. package/dist/main.js +84 -21
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -50,8 +50,17 @@ The agent's `moltnet.json` and gitconfig live next to each other in
50
50
 
51
51
  ### Pi provider auth
52
52
 
53
- Pi resolves provider credentials in this order: `~/.pi/agent/auth.json` (if
54
- present) wins, else environment variables. For CI prefer env vars:
53
+ The daemon resolves Pi config from the repository-local `.pi` directory by
54
+ default. On startup, if `PI_CODING_AGENT_DIR` is not already set, the daemon
55
+ sets it to `<repo-root>/.pi` before creating Pi sessions. This keeps daemon
56
+ runs deterministic and avoids inheriting user-level `~/.pi/agent` state.
57
+
58
+ Repo-local `.pi/settings.json` and `.pi/models.json` are intended to be
59
+ committed. `models.json` should reference provider keys by environment-variable
60
+ name, for example `"apiKey": "OLLAMA_API_KEY"`, not contain secret values.
61
+ Repo-local `.pi/auth.json` may exist for local subscription auth, but is
62
+ gitignored. Without `.pi/auth.json`, Pi falls back to environment-variable
63
+ provider keys:
55
64
 
56
65
  ```bash
57
66
  export ANTHROPIC_API_KEY=sk-ant-...
@@ -59,7 +68,8 @@ export ANTHROPIC_API_KEY=sk-ant-...
59
68
  # https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/env-api-keys.ts
60
69
  ```
61
70
 
62
- To use a non-default auth directory: `PI_CODING_AGENT_DIR=/abs/path/to/.pi/agent`.
71
+ To force a non-repo Pi directory, set
72
+ `PI_CODING_AGENT_DIR=/abs/path/to/.pi-or-agent-dir` before starting the daemon.
63
73
 
64
74
  ### Observability
65
75
 
@@ -126,11 +136,12 @@ registered task type; unknown task-type names remain invalid.
126
136
  ### Prerequisites
127
137
 
128
138
  - Docker running.
129
- - pi authenticated for the model provider you'll drive the daemon with
130
- (`~/.pi/agent/auth.json` should already contain entries for `anthropic`
131
- and/or `openai-codex` — set up via the normal pi/legreffier onboarding). The
132
- daemon does **not** read `ANTHROPIC_API_KEY` from env at the smoke-test path
133
- (CI is the exception — see [Pi provider auth](#pi-provider-auth) above).
139
+ - Pi config for the model provider you'll drive the daemon with. Local daemon
140
+ runs default `PI_CODING_AGENT_DIR` to repo-local `.pi`, so committed
141
+ `.pi/settings.json` and `.pi/models.json` must list the provider/model. For
142
+ subscription auth, put your local token blob in `.pi/auth.json`; it is
143
+ gitignored. For API-key auth, keep `.pi/auth.json` absent and export the
144
+ provider key referenced by `.pi/models.json`, for example `OLLAMA_API_KEY`.
134
145
  - `ssh-keygen` on `PATH`.
135
146
  - A `sandbox.json` at the repo root, or an explicit `--sandbox <path>` when
136
147
  starting the daemon. The daemon searches up for this file and uses its
package/dist/main.js CHANGED
@@ -31258,12 +31258,21 @@ if (!etc.sha512Sync) etc.sha512Sync = (...m) => {
31258
31258
  */
31259
31259
  async function isContinuationClaimableByThisDaemon(task, slotRegistry) {
31260
31260
  const cf = task.input?.continueFrom;
31261
- if (!cf) return true;
31261
+ if (!cf) return { claimable: true };
31262
31262
  const slot = await slotRegistry.findLatestProducerSlotByTaskAttempt(cf.taskId, cf.attemptN);
31263
- if (!slot) return false;
31263
+ if (!slot) return {
31264
+ claimable: false,
31265
+ reason: "missing_producer_slot",
31266
+ continueFrom: cf
31267
+ };
31264
31268
  const sessionDir = slot.session?.sessionDir;
31265
- if (!sessionDir || !existsSync(sessionDir)) return false;
31266
- return true;
31269
+ if (!sessionDir || !existsSync(sessionDir)) return {
31270
+ claimable: false,
31271
+ reason: "missing_session_dir",
31272
+ continueFrom: cf,
31273
+ sessionDir
31274
+ };
31275
+ return { claimable: true };
31267
31276
  }
31268
31277
  var DEFAULT_LIST_LIMIT = 10;
31269
31278
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -31351,7 +31360,19 @@ var PollingApiTaskSource = class {
31351
31360
  if (seen.has(item.id)) continue;
31352
31361
  if (this.opts.taskTypes && this.opts.taskTypes.length > 0 && !this.opts.taskTypes.includes(item.taskType)) continue;
31353
31362
  if (this.opts.diaryIds && this.opts.diaryIds.length > 0 && (item.diaryId === null || !this.opts.diaryIds.includes(item.diaryId))) continue;
31354
- if (this.opts.slotRegistry && !await isContinuationClaimableByThisDaemon(item, this.opts.slotRegistry)) continue;
31363
+ if (this.opts.slotRegistry) {
31364
+ const affinity = await isContinuationClaimableByThisDaemon(item, this.opts.slotRegistry);
31365
+ if (!affinity.claimable) {
31366
+ this.logger.debug({
31367
+ taskId: item.id,
31368
+ taskType: item.taskType,
31369
+ reason: affinity.reason,
31370
+ continueFrom: affinity.continueFrom,
31371
+ sessionDir: affinity.sessionDir
31372
+ }, "polling-api.continuation_skipped");
31373
+ continue;
31374
+ }
31375
+ }
31355
31376
  if (this.opts.profileId) {
31356
31377
  const allowed = item.allowedProfiles ?? [];
31357
31378
  if (allowed.length > 0 && !allowed.some((p) => p.profileId === this.opts.profileId)) continue;
@@ -32824,8 +32845,7 @@ async function resumeVm(config) {
32824
32845
  signal: config.signal
32825
32846
  });
32826
32847
  if (creds.gitconfig) {
32827
- const vmSigningKey = `${vmSshDir}/id_ed25519`;
32828
- const vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
32848
+ const vmGitconfig = rewriteGitconfigPaths(creds.gitconfig, vmSshDir, vmAgentDir);
32829
32849
  await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, {
32830
32850
  mode: 420,
32831
32851
  signal: config.signal
@@ -32848,17 +32868,6 @@ async function resumeVm(config) {
32848
32868
  signal: config.signal
32849
32869
  });
32850
32870
  await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet", { signal: config.signal });
32851
- const gitCredHelperPath = `${vmSshDir}/git-credential-moltnet`;
32852
- const credHelperScript = `#!/bin/sh
32853
- echo "username=x-access-token"
32854
- echo "password=$(moltnet github token --credentials ${vmSshDir}/moltnet.json)"
32855
- `;
32856
- await vm.fs.writeFile(gitCredHelperPath, credHelperScript, {
32857
- mode: 493,
32858
- signal: config.signal
32859
- });
32860
- await vmRun(vm, "git credential helper", `git config --global credential.helper ${gitCredHelperPath} && \
32861
- git config --global url."https://github.com/".insteadOf "git@github.com:"`, config.signal);
32862
32871
  return {
32863
32872
  vm,
32864
32873
  credentials: creds,
@@ -32877,6 +32886,30 @@ echo "password=$(moltnet github token --credentials ${vmSshDir}/moltnet.json)"
32877
32886
  }
32878
32887
  }
32879
32888
  /**
32889
+ * Rewrite host-absolute paths inside an agent gitconfig to VM-local
32890
+ * equivalents before injecting it into the guest.
32891
+ *
32892
+ * Two rewrites:
32893
+ * - `signingKey = <host path>` → `<vmSshDir>/id_ed25519`
32894
+ * - `... credential-helper --credentials <host moltnet.json>`
32895
+ * → `<vmAgentDir>/moltnet.json`
32896
+ *
32897
+ * The credential-helper line is generated host-side by `moltnet github setup`
32898
+ * with a host-absolute `--credentials` path; inside the guest that path is
32899
+ * invalid, so it must point at the VM-side moltnet.json. The `insteadOf`
32900
+ * rewrite rule and every other line are workspace-independent and pass through
32901
+ * unchanged. A gitconfig without a credential helper is rewritten only for
32902
+ * `signingKey`.
32903
+ *
32904
+ * This is the single source of truth for git push auth in the guest: the
32905
+ * injected gitconfig carries the tokenless mint-on-demand helper, so the VM
32906
+ * no longer hand-rolls a credential-helper script or runs an imperative
32907
+ * `git config --global ... insteadOf` against the guest $HOME.
32908
+ */
32909
+ function rewriteGitconfigPaths(gitconfig, vmSshDir, vmAgentDir) {
32910
+ return gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSshDir}/id_ed25519`).replace(/(moltnet github credential-helper --credentials )\S+/g, `$1${vmAgentDir}/moltnet.json`);
32911
+ }
32912
+ /**
32880
32913
  * Rewrite host-absolute paths inside moltnet.json to VM-local equivalents.
32881
32914
  *
32882
32915
  * Fields rewritten:
@@ -34785,9 +34818,13 @@ function loadConfig() {
34785
34818
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
34786
34819
  logLevel: process.env["LOG_LEVEL"] ?? "",
34787
34820
  profilePrerequisiteEnv: process.env,
34788
- profilePrerequisitePath: process.env.PATH ?? ""
34821
+ profilePrerequisitePath: process.env.PATH ?? "",
34822
+ piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? ""
34789
34823
  };
34790
34824
  }
34825
+ function activatePiCodingAgentDir(path) {
34826
+ process.env["PI_CODING_AGENT_DIR"] = path;
34827
+ }
34791
34828
  //#endregion
34792
34829
  //#region src/lib/agent-context.ts
34793
34830
  /**
@@ -35052,6 +35089,7 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
35052
35089
  return {
35053
35090
  ...basePlan,
35054
35091
  workspaceMode: "dedicated_worktree",
35092
+ workspaceId: resolution.producerSlot.workspace?.workspaceId ?? null,
35055
35093
  worktreeBranch: resolution.producerSlot.workspace?.worktreeBranch ?? null,
35056
35094
  sessionPersistence: {
35057
35095
  sessionDir: `${stateDirs.piSessionsDir}/continue-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`,
@@ -35374,6 +35412,23 @@ async function initWorkerOtel(options) {
35374
35412
  };
35375
35413
  }
35376
35414
  //#endregion
35415
+ //#region src/lib/pi-agent-dir.ts
35416
+ function ensurePiAgentDir(repoRoot, explicitPath) {
35417
+ if (explicitPath) {
35418
+ mkdirSync(explicitPath, { recursive: true });
35419
+ return {
35420
+ path: explicitPath,
35421
+ source: "env"
35422
+ };
35423
+ }
35424
+ const path = join(repoRoot, ".pi");
35425
+ mkdirSync(path, { recursive: true });
35426
+ return {
35427
+ path,
35428
+ source: "repo"
35429
+ };
35430
+ }
35431
+ //#endregion
35377
35432
  //#region src/lib/runtime-profile.ts
35378
35433
  var RuntimeProfilePrerequisiteError = class extends Error {
35379
35434
  constructor(profileName, missingEnv, missingTools) {
@@ -35614,6 +35669,8 @@ async function runPolling(opts) {
35614
35669
  rootDir: profile.mountPath,
35615
35670
  path: profile.source
35616
35671
  } : resolveSandbox(process.cwd(), values.sandbox);
35672
+ const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
35673
+ activatePiCodingAgentDir(piAgentDir.path);
35617
35674
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
35618
35675
  const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
35619
35676
  const slotIdentity = {
@@ -35691,7 +35748,9 @@ async function runPolling(opts) {
35691
35748
  profileId: profile.id,
35692
35749
  profileSessionTtlSec: profile.sessionTtlSec,
35693
35750
  profileWorkspaceTtlSec: profile.workspaceTtlSec
35694
- } : {}
35751
+ } : {},
35752
+ piAgentDir: piAgentDir.path,
35753
+ piAgentDirSource: piAgentDir.source
35695
35754
  }, "agent-daemon.starting");
35696
35755
  const outputs = [];
35697
35756
  try {
@@ -35967,6 +36026,8 @@ async function runOnce(argv) {
35967
36026
  rootDir: profile.mountPath,
35968
36027
  path: profile.source
35969
36028
  } : resolveSandbox(process.cwd(), values.sandbox);
36029
+ const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
36030
+ activatePiCodingAgentDir(piAgentDir.path);
35970
36031
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
35971
36032
  const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
35972
36033
  const slotIdentity = {
@@ -36017,7 +36078,9 @@ async function runOnce(argv) {
36017
36078
  profileId: profile.id,
36018
36079
  profileSessionTtlSec: profile.sessionTtlSec,
36019
36080
  profileWorkspaceTtlSec: profile.workspaceTtlSec
36020
- } : {}
36081
+ } : {},
36082
+ piAgentDir: piAgentDir.path,
36083
+ piAgentDirSource: piAgentDir.source
36021
36084
  }, "agent-daemon.starting");
36022
36085
  let runtime = null;
36023
36086
  let activeAttemptN = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
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.",
@@ -45,10 +45,10 @@
45
45
  "@opentelemetry/semantic-conventions": "^1.39.0",
46
46
  "pino": "^10.3.1",
47
47
  "pino-pretty": "^13.1.3",
48
- "@themoltnet/agent-daemon-state": "0.2.0",
48
+ "@themoltnet/agent-runtime": "0.25.0",
49
49
  "@themoltnet/sdk": "0.108.0",
50
- "@themoltnet/agent-runtime": "0.24.0",
51
- "@themoltnet/pi-extension": "0.23.1"
50
+ "@themoltnet/pi-extension": "0.24.1",
51
+ "@themoltnet/agent-daemon-state": "0.2.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsx": "^4.7.0",
@@ -57,8 +57,8 @@
57
57
  "vitest": "^3.0.0",
58
58
  "@moltnet/bootstrap": "0.1.0",
59
59
  "@moltnet/crypto-service": "0.1.0",
60
- "@moltnet/observability": "0.1.0",
61
- "@moltnet/tasks": "0.1.0"
60
+ "@moltnet/tasks": "0.1.0",
61
+ "@moltnet/observability": "0.1.0"
62
62
  },
63
63
  "nx": {
64
64
  "tags": [