@themoltnet/agent-daemon 0.18.0 → 0.19.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 +564 -332
  2. package/package.json +6 -6
package/dist/main.js CHANGED
@@ -8,12 +8,13 @@ import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
8
8
  import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";
9
9
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
10
10
  import crypto$1, { createHash } from "crypto";
11
- import { createHash as createHash$1 } from "node:crypto";
11
+ import crypto$2, { createHash as createHash$1 } from "node:crypto";
12
12
  import path, { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
13
  import { parseArgs, parseEnv, promisify } from "node:util";
14
14
  import { execFile, execFileSync } from "node:child_process";
15
- import { accessSync, constants, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
15
+ import fs, { accessSync, constants, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
16
16
  import { DatabaseSync } from "node:sqlite";
17
+ import { fileURLToPath } from "node:url";
17
18
  import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
18
19
  import { pino, transport } from "pino";
19
20
  import { readFile } from "node:fs/promises";
@@ -4375,6 +4376,54 @@ function validateRubricWeights(rubric) {
4375
4376
  return null;
4376
4377
  }
4377
4378
  //#endregion
4379
+ //#region ../../libs/tasks/src/runtime-models.ts
4380
+ /**
4381
+ * Runtime model catalog: a list of supported provider/model couples that
4382
+ * MoltNet daemons can target. Backed by the `runtime_models` table.
4383
+ *
4384
+ * Scope is intrinsic to the row:
4385
+ * - `teamId == null` => global entry (MoltNet-seeded, read-only to most callers)
4386
+ * - `teamId != null` => team-owned custom entry
4387
+ *
4388
+ * The REST API exposes a single shape regardless of scope; the team header
4389
+ * gates which rows are returned.
4390
+ */
4391
+ var RuntimeModelProvider = String$1({
4392
+ minLength: 1,
4393
+ maxLength: 100,
4394
+ pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$"
4395
+ });
4396
+ var RuntimeModelName = String$1({
4397
+ minLength: 1,
4398
+ maxLength: 200,
4399
+ pattern: "^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,199}$"
4400
+ });
4401
+ var RuntimeModelCapabilities = Record(String$1({
4402
+ minLength: 1,
4403
+ maxLength: 64
4404
+ }), Union([
4405
+ Boolean$1(),
4406
+ Number$1(),
4407
+ String$1({ maxLength: 256 })
4408
+ ]));
4409
+ _Object_({
4410
+ id: String$1({ format: "uuid" }),
4411
+ teamId: Union([String$1({ format: "uuid" }), Null()]),
4412
+ provider: RuntimeModelProvider,
4413
+ model: RuntimeModelName,
4414
+ displayName: Union([String$1({ maxLength: 200 }), Null()]),
4415
+ description: Union([String$1({ maxLength: 4096 }), Null()]),
4416
+ capabilities: RuntimeModelCapabilities,
4417
+ isActive: Boolean$1(),
4418
+ createdByAgentId: Union([String$1({ format: "uuid" }), Null()]),
4419
+ createdByHumanId: Union([String$1({ format: "uuid" }), Null()]),
4420
+ createdAt: String$1({ format: "date-time" }),
4421
+ updatedAt: String$1({ format: "date-time" })
4422
+ }, {
4423
+ $id: "RuntimeModel",
4424
+ additionalProperties: false
4425
+ });
4426
+ //#endregion
4378
4427
  //#region ../../libs/tasks/src/runtime-profiles.ts
4379
4428
  var RuntimeProfileName = String$1({
4380
4429
  minLength: 1,
@@ -5756,20 +5805,17 @@ var FreeformOutput = _Object_({
5756
5805
  * 3. `freeform.sourceAttemptNotCompleted` — named attempt is missing
5757
5806
  * or not in `completed` state; warm continuation only makes sense
5758
5807
  * once the parent has produced a terminal output.
5759
- * 4. `freeform.forkModeNotImplemented` — `mode: 'fork'` is the wire
5760
- * surface for copy-on-write continuation tracked in #1293; v1
5761
- * rejects it server-side so daemons never have to branch.
5762
- * 5. `freeform.executionWorkspaceNotInheritable` — caller set
5808
+ * 4. `freeform.executionWorkspaceNotInheritable` — caller set
5763
5809
  * `execution.workspace` together with `continueFrom`. Workspace
5764
5810
  * mode for a continuation is inherited from the parent slot
5765
5811
  * (`maybeAttachWarmSlotContext` forces `dedicated_worktree` +
5766
5812
  * the parent's worktreeBranch), so any caller-supplied override
5767
5813
  * is silently dropped at the daemon plan stage. Reject explicitly
5768
5814
  * so misconfiguration surfaces at create time.
5769
- * 6. `freeform.sourceNotResumeEligible` — `daemonState` is null or
5815
+ * 5. `freeform.sourceNotResumeEligible` — `daemonState` is null or
5770
5816
  * `slotResumableUntil` is null. Older completions (pre-#1287) and
5771
5817
  * daemons that opt out fall here.
5772
- * 7. `freeform.sourceResumeExpired` — `slotResumableUntil` is in the
5818
+ * 6. `freeform.sourceResumeExpired` — `slotResumableUntil` is in the
5773
5819
  * past; the warm slot's TTL has elapsed and no daemon is
5774
5820
  * guaranteed to still hold it.
5775
5821
  *
@@ -5790,11 +5836,6 @@ async function validateFreeformInputAsync(input, ctx) {
5790
5836
  message: `Source task type '${source.taskType}' is not continuable; only freeform → freeform is supported in v1`,
5791
5837
  code: "freeform.sourceTaskTypeNotSupported"
5792
5838
  }];
5793
- if (cf.mode === "fork") return [{
5794
- field: "input/continueFrom/mode",
5795
- message: "fork mode not yet implemented; see https://github.com/getlarge/themoltnet/issues/1293",
5796
- code: "freeform.forkModeNotImplemented"
5797
- }];
5798
5839
  if (input.execution?.workspace) return [{
5799
5840
  field: "input/execution/workspace",
5800
5841
  message: "execution.workspace is inherited from the parent slot when continueFrom is set; omit it",
@@ -8041,7 +8082,7 @@ function Errors$1(...args) {
8041
8082
  //#endregion
8042
8083
  //#region ../../node_modules/.pnpm/typebox@1.2.8/node_modules/typebox/build/schema/check.mjs
8043
8084
  /** Checks a value against the provided schema */
8044
- function Check$1(...args) {
8085
+ function Check$3(...args) {
8045
8086
  const [context, schema, value] = Match$3(args, {
8046
8087
  3: (context, schema, value) => [
8047
8088
  context,
@@ -8059,7 +8100,7 @@ function Check$1(...args) {
8059
8100
  //#endregion
8060
8101
  //#region ../../node_modules/.pnpm/typebox@1.2.8/node_modules/typebox/build/value/check/check.mjs
8061
8102
  /** Checks a value matches the provided type. */
8062
- function Check(...args) {
8103
+ function Check$2(...args) {
8063
8104
  const [context, type, value] = Match$3(args, {
8064
8105
  3: (context, type, value) => [
8065
8106
  context,
@@ -8072,7 +8113,7 @@ function Check(...args) {
8072
8113
  value
8073
8114
  ]
8074
8115
  });
8075
- return Check$1(context, type, value);
8116
+ return Check$3(context, type, value);
8076
8117
  }
8077
8118
  //#endregion
8078
8119
  //#region ../../node_modules/.pnpm/typebox@1.2.8/node_modules/typebox/build/value/errors/errors.mjs
@@ -8159,7 +8200,7 @@ function FromObject$4(context, type, value) {
8159
8200
  value[key] = FromType$3(context, type.properties[key], value[key]);
8160
8201
  continue;
8161
8202
  }
8162
- if (IsBoolean$2(additionalProperties) && IsEqual(additionalProperties, true) || IsSchema$1(additionalProperties) && Check(context, additionalProperties, value[key])) {
8203
+ if (IsBoolean$2(additionalProperties) && IsEqual(additionalProperties, true) || IsSchema$1(additionalProperties) && Check$2(context, additionalProperties, value[key])) {
8163
8204
  value[key] = FromType$3(context, additionalProperties, value[key]);
8164
8205
  continue;
8165
8206
  }
@@ -8178,7 +8219,7 @@ function FromRecord$3(context, type, value) {
8178
8219
  value[key] = FromType$3(context, recordValue, value[key]);
8179
8220
  continue;
8180
8221
  }
8181
- if (IsBoolean$2(additionalProperties) && IsEqual(additionalProperties, true) || IsSchema$1(additionalProperties) && Check(context, additionalProperties, value[key])) {
8222
+ if (IsBoolean$2(additionalProperties) && IsEqual(additionalProperties, true) || IsSchema$1(additionalProperties) && Check$2(context, additionalProperties, value[key])) {
8182
8223
  value[key] = FromType$3(context, additionalProperties, value[key]);
8183
8224
  continue;
8184
8225
  }
@@ -8261,7 +8302,7 @@ function UnionPrioritySort(types, order = 1) {
8261
8302
  function FromUnion$3(context, type, value) {
8262
8303
  for (const schema of UnionPrioritySort(type.anyOf)) {
8263
8304
  const clean = FromType$3(context, schema, Clone(value));
8264
- if (Check(context, schema, clean)) return clean;
8305
+ if (Check$2(context, schema, clean)) return clean;
8265
8306
  }
8266
8307
  return value;
8267
8308
  }
@@ -8562,8 +8603,8 @@ function FromUndefined(_context, _type, value) {
8562
8603
  //#endregion
8563
8604
  //#region ../../node_modules/.pnpm/typebox@1.2.8/node_modules/typebox/build/value/convert/from_union.mjs
8564
8605
  function FromUnion$2(context, type, value) {
8565
- if (type.anyOf.some((type) => Check(context, type, value))) return value;
8566
- const selected = type.anyOf.map((type) => FromType$2(context, type, Clone(value))).find((value) => Check(context, type, value));
8606
+ if (type.anyOf.some((type) => Check$2(context, type, value))) return value;
8607
+ const selected = type.anyOf.map((type) => FromType$2(context, type, Clone(value))).find((value) => Check$2(context, type, value));
8567
8608
  return IsUndefined$1(selected) ? value : selected;
8568
8609
  }
8569
8610
  //#endregion
@@ -8680,7 +8721,7 @@ function FromTuple$1(context, schema, value) {
8680
8721
  function FromUnion$1(context, schema, value) {
8681
8722
  for (const inner of schema.anyOf) {
8682
8723
  const result = FromType$1(context, inner, Clone(value));
8683
- if (Check(context, inner, result)) return result;
8724
+ if (Check$2(context, inner, result)) return result;
8684
8725
  }
8685
8726
  return value;
8686
8727
  }
@@ -8870,7 +8911,7 @@ function FromTuple(direction, context, type, value) {
8870
8911
  //#region ../../node_modules/.pnpm/typebox@1.2.8/node_modules/typebox/build/value/codec/from_union.mjs
8871
8912
  function Decode(direction, context, type, value) {
8872
8913
  for (const schema of UnionPrioritySort(type.anyOf, 1)) {
8873
- if (!Check(context, schema, value)) continue;
8914
+ if (!Check$2(context, schema, value)) continue;
8874
8915
  return Callback(direction, context, type, FromType(direction, context, schema, value));
8875
8916
  }
8876
8917
  return value;
@@ -8879,7 +8920,7 @@ function Encode(direction, context, type, value) {
8879
8920
  const exterior = Callback(direction, context, type, value);
8880
8921
  for (const schema of UnionPrioritySort(type.anyOf, -1)) {
8881
8922
  const variant = FromType(direction, context, schema, Clone(exterior));
8882
- if (!Check(context, schema, variant)) continue;
8923
+ if (!Check$2(context, schema, variant)) continue;
8883
8924
  return variant;
8884
8925
  }
8885
8926
  return exterior;
@@ -8900,7 +8941,7 @@ var DecodeError = class extends AssertError {
8900
8941
  }
8901
8942
  };
8902
8943
  function Assert$2(context, type, value) {
8903
- if (!Check(context, type, value)) throw new DecodeError(value, Errors(context, type, value));
8944
+ if (!Check$2(context, type, value)) throw new DecodeError(value, Errors(context, type, value));
8904
8945
  return value;
8905
8946
  }
8906
8947
  /** Executes Decode callbacks only */
@@ -8923,7 +8964,7 @@ var EncodeError = class extends AssertError {
8923
8964
  }
8924
8965
  };
8925
8966
  function Assert$1(context, type, value) {
8926
- if (!Check(context, type, value)) throw new EncodeError(value, Errors(context, type, value));
8967
+ if (!Check$2(context, type, value)) throw new EncodeError(value, Errors(context, type, value));
8927
8968
  return value;
8928
8969
  }
8929
8970
  /** Executes Encode callbacks only */
@@ -8946,7 +8987,7 @@ var ParseError = class extends AssertError {
8946
8987
  }
8947
8988
  };
8948
8989
  function Assert(context, type, value) {
8949
- if (!Check(context, type, value)) throw new ParseError(value, Errors(context, type, value));
8990
+ if (!Check$2(context, type, value)) throw new ParseError(value, Errors(context, type, value));
8950
8991
  return value;
8951
8992
  }
8952
8993
  Pipeline([
@@ -9763,6 +9804,17 @@ var ForeignKey$1 = class {
9763
9804
  return name ?? `${chunks.join("_")}_fk`;
9764
9805
  }
9765
9806
  };
9807
+ function foreignKey$1(config) {
9808
+ function mappedConfig() {
9809
+ const { name, columns, foreignColumns } = config;
9810
+ return {
9811
+ name,
9812
+ columns,
9813
+ foreignColumns
9814
+ };
9815
+ }
9816
+ return new ForeignKeyBuilder$1(mappedConfig);
9817
+ }
9766
9818
  //#endregion
9767
9819
  //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_@types+pg@8.16_374a03c8dde600dcef5a1d8002afafad/node_modules/drizzle-orm/tracing-utils.js
9768
9820
  function iife(fn, ...args) {
@@ -15270,12 +15322,12 @@ var require_helper = /* @__PURE__ */ __commonJSMin(((exports, module) => {
15270
15322
  //#region ../../node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/index.js
15271
15323
  var require_lib$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
15272
15324
  __require("path");
15273
- var fs = __require("fs"), helper = require_helper();
15325
+ var fs$1 = __require("fs"), helper = require_helper();
15274
15326
  module.exports = function(connInfo, cb) {
15275
15327
  var file = helper.getFileName();
15276
- fs.stat(file, function(err, stat) {
15328
+ fs$1.stat(file, function(err, stat) {
15277
15329
  if (err || !helper.usePgPass(stat, file)) return cb(void 0);
15278
- var st = fs.createReadStream(file);
15330
+ var st = fs$1.createReadStream(file);
15279
15331
  helper.getPassword(connInfo, st, cb);
15280
15332
  });
15281
15333
  };
@@ -16534,6 +16586,33 @@ var SelectionProxyHandler = class SelectionProxyHandler {
16534
16586
  }
16535
16587
  };
16536
16588
  //#endregion
16589
+ //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_@types+pg@8.16_374a03c8dde600dcef5a1d8002afafad/node_modules/drizzle-orm/pg-core/checks.js
16590
+ var CheckBuilder$1 = class {
16591
+ constructor(name, value) {
16592
+ this.name = name;
16593
+ this.value = value;
16594
+ }
16595
+ static [entityKind] = "PgCheckBuilder";
16596
+ brand;
16597
+ /** @internal */
16598
+ build(table) {
16599
+ return new Check$1(table, this);
16600
+ }
16601
+ };
16602
+ var Check$1 = class {
16603
+ constructor(table, builder) {
16604
+ this.table = table;
16605
+ this.name = builder.name;
16606
+ this.value = builder.value;
16607
+ }
16608
+ static [entityKind] = "PgCheck";
16609
+ name;
16610
+ value;
16611
+ };
16612
+ function check$1(name, value) {
16613
+ return new CheckBuilder$1(name, value);
16614
+ }
16615
+ //#endregion
16537
16616
  //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_@types+pg@8.16_374a03c8dde600dcef5a1d8002afafad/node_modules/drizzle-orm/pg-core/indexes.js
16538
16617
  var IndexBuilderOn$1 = class {
16539
16618
  constructor(unique, name) {
@@ -19335,6 +19414,20 @@ var ForeignKey = class {
19335
19414
  return name ?? `${chunks.join("_")}_fk`;
19336
19415
  }
19337
19416
  };
19417
+ function foreignKey(config) {
19418
+ function mappedConfig() {
19419
+ if (typeof config === "function") {
19420
+ const { name, columns, foreignColumns } = config();
19421
+ return {
19422
+ name,
19423
+ columns,
19424
+ foreignColumns
19425
+ };
19426
+ }
19427
+ return config;
19428
+ }
19429
+ return new ForeignKeyBuilder(mappedConfig);
19430
+ }
19338
19431
  //#endregion
19339
19432
  //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_@types+pg@8.16_374a03c8dde600dcef5a1d8002afafad/node_modules/drizzle-orm/sqlite-core/unique-constraint.js
19340
19433
  function uniqueKeyName(table, columns) {
@@ -19787,6 +19880,32 @@ var sqliteTable = (name, columns, extraConfig) => {
19787
19880
  return sqliteTableBase(name, columns, extraConfig);
19788
19881
  };
19789
19882
  //#endregion
19883
+ //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_@types+pg@8.16_374a03c8dde600dcef5a1d8002afafad/node_modules/drizzle-orm/sqlite-core/checks.js
19884
+ var CheckBuilder = class {
19885
+ constructor(name, value) {
19886
+ this.name = name;
19887
+ this.value = value;
19888
+ }
19889
+ static [entityKind] = "SQLiteCheckBuilder";
19890
+ brand;
19891
+ build(table) {
19892
+ return new Check(table, this);
19893
+ }
19894
+ };
19895
+ var Check = class {
19896
+ constructor(table, builder) {
19897
+ this.table = table;
19898
+ this.name = builder.name;
19899
+ this.value = builder.value;
19900
+ }
19901
+ static [entityKind] = "SQLiteCheck";
19902
+ name;
19903
+ value;
19904
+ };
19905
+ function check(name, value) {
19906
+ return new CheckBuilder(name, value);
19907
+ }
19908
+ //#endregion
19790
19909
  //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_@types+pg@8.16_374a03c8dde600dcef5a1d8002afafad/node_modules/drizzle-orm/sqlite-core/indexes.js
19791
19910
  var IndexBuilderOn = class {
19792
19911
  constructor(name, unique) {
@@ -22188,7 +22307,136 @@ function drizzle(callback, batchCallback, config) {
22188
22307
  return db;
22189
22308
  }
22190
22309
  //#endregion
22310
+ //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_@types+pg@8.16_374a03c8dde600dcef5a1d8002afafad/node_modules/drizzle-orm/migrator.js
22311
+ function readMigrationFiles(config) {
22312
+ const migrationFolderTo = config.migrationsFolder;
22313
+ const migrationQueries = [];
22314
+ const journalPath = `${migrationFolderTo}/meta/_journal.json`;
22315
+ if (!fs.existsSync(journalPath)) throw new Error(`Can't find meta/_journal.json file`);
22316
+ const journalAsString = fs.readFileSync(`${migrationFolderTo}/meta/_journal.json`).toString();
22317
+ const journal = JSON.parse(journalAsString);
22318
+ for (const journalEntry of journal.entries) {
22319
+ const migrationPath = `${migrationFolderTo}/${journalEntry.tag}.sql`;
22320
+ try {
22321
+ const query = fs.readFileSync(`${migrationFolderTo}/${journalEntry.tag}.sql`).toString();
22322
+ const result = query.split("--> statement-breakpoint").map((it) => {
22323
+ return it;
22324
+ });
22325
+ migrationQueries.push({
22326
+ sql: result,
22327
+ bps: journalEntry.breakpoints,
22328
+ folderMillis: journalEntry.when,
22329
+ hash: crypto$2.createHash("sha256").update(query).digest("hex")
22330
+ });
22331
+ } catch {
22332
+ throw new Error(`No file ${migrationPath} found in ${migrationFolderTo} folder`);
22333
+ }
22334
+ }
22335
+ return migrationQueries;
22336
+ }
22337
+ //#endregion
22338
+ //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_@types+pg@8.16_374a03c8dde600dcef5a1d8002afafad/node_modules/drizzle-orm/node-postgres/migrator.js
22339
+ async function migrate(db, config) {
22340
+ const migrations = readMigrationFiles(config);
22341
+ await db.dialect.migrate(migrations, db.session, config);
22342
+ }
22343
+ //#endregion
22344
+ //#region ../../libs/agent-daemon-state/src/migrate.ts
22345
+ var __dirname = dirname(fileURLToPath(import.meta.url));
22346
+ /**
22347
+ * Walk up from this file to find a generated drizzle migrations folder.
22348
+ * Works from both `src/` (tsx dev) and `dist/src/` (compiled).
22349
+ */
22350
+ function findMigrationsFolder(folderName) {
22351
+ let dir = __dirname;
22352
+ for (let i = 0; i < 5; i++) {
22353
+ const candidate = resolve(dir, folderName);
22354
+ if (existsSync(candidate)) return candidate;
22355
+ dir = dirname(dir);
22356
+ }
22357
+ return resolve(__dirname, "..", folderName);
22358
+ }
22359
+ /** Statements in a generated drizzle SQL file are joined by this marker. */
22360
+ var STATEMENT_BREAKPOINT = "--> statement-breakpoint";
22361
+ function readMigrationStatements(folder) {
22362
+ const files = readdirSync(folder).filter((name) => name.endsWith(".sql")).sort();
22363
+ const statements = [];
22364
+ for (const file of files) {
22365
+ const raw = readFileSync(resolve(folder, file), "utf8");
22366
+ for (const chunk of raw.split(STATEMENT_BREAKPOINT)) {
22367
+ const trimmed = chunk.trim();
22368
+ if (trimmed.length > 0) statements.push(trimmed);
22369
+ }
22370
+ }
22371
+ return statements;
22372
+ }
22373
+ /**
22374
+ * Apply the SQLite baseline synchronously via the driver's `exec`.
22375
+ *
22376
+ * Fresh-start contract: the baseline is authored to run against an EMPTY
22377
+ * database. The daemon-state DB is a local warm-slot cache, and this migrator
22378
+ * does not adopt schemas created by the previous inline `CREATE TABLE IF NOT
22379
+ * EXISTS` bootstrap — upgrading from that path requires deleting the local
22380
+ * `daemon-state.sqlite` first (or dropping the Postgres tables). This is a
22381
+ * deliberate scope decision: there is no durable data to preserve, so in-place
22382
+ * adoption of legacy schemas is intentionally not implemented.
22383
+ *
22384
+ * The daemon-state SQLite store is constructed synchronously (callers do not
22385
+ * await it), and the runtime driver is a `node:sqlite` `DatabaseSync` wrapped
22386
+ * in drizzle's sqlite-proxy — which has no file-based migrator. We therefore
22387
+ * apply the drizzle-kit-generated SQL ourselves: the generated file stays the
22388
+ * single source of truth, we just run it synchronously. Idempotent via a
22389
+ * tracking table so re-opening an existing DB is a no-op.
22390
+ */
22391
+ function applySqliteMigrations(client, migrationsFolder = findMigrationsFolder("drizzle-sqlite")) {
22392
+ client.exec(`
22393
+ CREATE TABLE IF NOT EXISTS __daemon_state_migrations (
22394
+ statement_index INTEGER PRIMARY KEY,
22395
+ applied_at_ms INTEGER NOT NULL
22396
+ )
22397
+ `);
22398
+ const alreadyApplied = client.prepare(`SELECT COALESCE(MAX(statement_index), -1) AS last FROM __daemon_state_migrations`).get().last;
22399
+ const statements = readMigrationStatements(migrationsFolder);
22400
+ if (statements.length <= alreadyApplied + 1) return;
22401
+ client.exec("BEGIN IMMEDIATE");
22402
+ try {
22403
+ for (let i = alreadyApplied + 1; i < statements.length; i++) {
22404
+ client.exec(statements[i]);
22405
+ client.prepare(`INSERT INTO __daemon_state_migrations (statement_index, applied_at_ms)
22406
+ VALUES (?, ?)`).run(i, Date.now());
22407
+ }
22408
+ client.exec("COMMIT");
22409
+ } catch (error) {
22410
+ try {
22411
+ client.exec("ROLLBACK");
22412
+ } catch {}
22413
+ throw error;
22414
+ }
22415
+ }
22416
+ /**
22417
+ * Run the Postgres baseline via the standard node-postgres migrator (tracked,
22418
+ * transactional, idempotent). Mirrors `libs/database/src/migrate.ts`. Used when
22419
+ * the daemon points at a shared Postgres instance instead of a local SQLite
22420
+ * file.
22421
+ */
22422
+ async function runPgMigrations(pool) {
22423
+ await migrate(drizzle$1(pool), { migrationsFolder: findMigrationsFolder("drizzle-pg") });
22424
+ }
22425
+ //#endregion
22191
22426
  //#region ../../libs/agent-daemon-state/src/pg-schema.ts
22427
+ var pgDaemonWorkspaces = pgTable("daemon_workspaces", {
22428
+ workspaceId: text$1("workspace_id").primaryKey(),
22429
+ worktreePath: text$1("worktree_path").notNull(),
22430
+ worktreeBranch: text$1("worktree_branch"),
22431
+ kind: text$1("kind", { enum: [
22432
+ "origin",
22433
+ "fork",
22434
+ "scratch"
22435
+ ] }).notNull(),
22436
+ refcount: integer$1("refcount").notNull().default(0),
22437
+ createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
22438
+ lastUsedAtMs: bigint("last_used_at_ms", { mode: "number" }).notNull()
22439
+ }, (table) => [check$1("daemon_workspaces_kind_check", sql`${table.kind} IN ('origin', 'fork', 'scratch')`)]);
22192
22440
  var pgDaemonSlots = pgTable("daemon_slots", {
22193
22441
  agentName: text$1("agent_name").notNull(),
22194
22442
  provider: text$1("provider").notNull(),
@@ -22198,6 +22446,7 @@ var pgDaemonSlots = pgTable("daemon_slots", {
22198
22446
  state: text$1("state", { enum: ["active", "idle"] }).notNull(),
22199
22447
  lastTaskId: text$1("last_task_id").notNull(),
22200
22448
  lastAttemptN: integer$1("last_attempt_n").notNull(),
22449
+ workspaceId: text$1("workspace_id").references(() => pgDaemonWorkspaces.workspaceId, { onDelete: "set null" }),
22201
22450
  createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
22202
22451
  lastUsedAtMs: bigint("last_used_at_ms", { mode: "number" }).notNull(),
22203
22452
  expiresAtMs: bigint("expires_at_ms", { mode: "number" }).notNull()
@@ -22208,8 +22457,9 @@ var pgDaemonSlots = pgTable("daemon_slots", {
22208
22457
  table.model,
22209
22458
  table.slotKey
22210
22459
  ] }),
22460
+ check$1("daemon_slots_state_check", sql`${table.state} IN ('active', 'idle')`),
22211
22461
  index$1("daemon_slots_expires_idx").on(table.expiresAtMs),
22212
- index$1("daemon_slots_task_attempt_idx").on(table.lastTaskId, table.lastAttemptN, table.lastUsedAtMs)
22462
+ index$1("daemon_slots_task_attempt_idx").on(table.lastTaskId, table.lastAttemptN, table.lastUsedAtMs.desc())
22213
22463
  ]);
22214
22464
  var pgDaemonSlotSessions = pgTable("daemon_slot_sessions", {
22215
22465
  agentName: text$1("agent_name").notNull(),
@@ -22223,28 +22473,40 @@ var pgDaemonSlotSessions = pgTable("daemon_slot_sessions", {
22223
22473
  table.provider,
22224
22474
  table.model,
22225
22475
  table.slotKey
22226
- ] })]);
22227
- var pgDaemonSlotWorkspaces = pgTable("daemon_slot_workspaces", {
22228
- agentName: text$1("agent_name").notNull(),
22229
- provider: text$1("provider").notNull(),
22230
- model: text$1("model").notNull(),
22231
- slotKey: text$1("slot_key").notNull(),
22232
- workspaceId: text$1("workspace_id").notNull().unique(),
22233
- worktreePath: text$1("worktree_path").notNull(),
22234
- worktreeBranch: text$1("worktree_branch")
22235
- }, (table) => [primaryKey$1({ columns: [
22236
- table.agentName,
22237
- table.provider,
22238
- table.model,
22239
- table.slotKey
22240
- ] })]);
22476
+ ] }), foreignKey$1({
22477
+ columns: [
22478
+ table.agentName,
22479
+ table.provider,
22480
+ table.model,
22481
+ table.slotKey
22482
+ ],
22483
+ foreignColumns: [
22484
+ pgDaemonSlots.agentName,
22485
+ pgDaemonSlots.provider,
22486
+ pgDaemonSlots.model,
22487
+ pgDaemonSlots.slotKey
22488
+ ]
22489
+ }).onDelete("cascade")]);
22241
22490
  var pgAgentDaemonStateSchema = {
22242
22491
  daemonSlotSessions: pgDaemonSlotSessions,
22243
- daemonSlotWorkspaces: pgDaemonSlotWorkspaces,
22492
+ daemonWorkspaces: pgDaemonWorkspaces,
22244
22493
  daemonSlots: pgDaemonSlots
22245
22494
  };
22246
22495
  //#endregion
22247
22496
  //#region ../../libs/agent-daemon-state/src/schema.ts
22497
+ var daemonWorkspaces = sqliteTable("daemon_workspaces", {
22498
+ workspaceId: text("workspace_id").primaryKey(),
22499
+ worktreePath: text("worktree_path").notNull(),
22500
+ worktreeBranch: text("worktree_branch"),
22501
+ kind: text("kind", { enum: [
22502
+ "origin",
22503
+ "fork",
22504
+ "scratch"
22505
+ ] }).notNull(),
22506
+ refcount: integer("refcount").notNull().default(0),
22507
+ createdAtMs: integer("created_at_ms").notNull(),
22508
+ lastUsedAtMs: integer("last_used_at_ms").notNull()
22509
+ }, (table) => [check("daemon_workspaces_kind_check", sql`${table.kind} IN ('origin', 'fork', 'scratch')`)]);
22248
22510
  var daemonSlots = sqliteTable("daemon_slots", {
22249
22511
  agentName: text("agent_name").notNull(),
22250
22512
  provider: text("provider").notNull(),
@@ -22254,6 +22516,7 @@ var daemonSlots = sqliteTable("daemon_slots", {
22254
22516
  state: text("state", { enum: ["active", "idle"] }).notNull(),
22255
22517
  lastTaskId: text("last_task_id").notNull(),
22256
22518
  lastAttemptN: integer("last_attempt_n").notNull(),
22519
+ workspaceId: text("workspace_id").references(() => daemonWorkspaces.workspaceId, { onDelete: "set null" }),
22257
22520
  createdAtMs: integer("created_at_ms").notNull(),
22258
22521
  lastUsedAtMs: integer("last_used_at_ms").notNull(),
22259
22522
  expiresAtMs: integer("expires_at_ms").notNull()
@@ -22264,8 +22527,9 @@ var daemonSlots = sqliteTable("daemon_slots", {
22264
22527
  table.model,
22265
22528
  table.slotKey
22266
22529
  ] }),
22530
+ check("daemon_slots_state_check", sql`${table.state} IN ('active', 'idle')`),
22267
22531
  index("daemon_slots_expires_idx").on(table.expiresAtMs),
22268
- index("daemon_slots_task_attempt_idx").on(table.lastTaskId, table.lastAttemptN, table.lastUsedAtMs)
22532
+ index("daemon_slots_task_attempt_idx").on(table.lastTaskId, table.lastAttemptN, sql`${table.lastUsedAtMs} DESC`)
22269
22533
  ]);
22270
22534
  var daemonSlotSessions = sqliteTable("daemon_slot_sessions", {
22271
22535
  agentName: text("agent_name").notNull(),
@@ -22279,24 +22543,23 @@ var daemonSlotSessions = sqliteTable("daemon_slot_sessions", {
22279
22543
  table.provider,
22280
22544
  table.model,
22281
22545
  table.slotKey
22282
- ] })]);
22283
- var daemonSlotWorkspaces = sqliteTable("daemon_slot_workspaces", {
22284
- agentName: text("agent_name").notNull(),
22285
- provider: text("provider").notNull(),
22286
- model: text("model").notNull(),
22287
- slotKey: text("slot_key").notNull(),
22288
- workspaceId: text("workspace_id").notNull().unique(),
22289
- worktreePath: text("worktree_path").notNull(),
22290
- worktreeBranch: text("worktree_branch")
22291
- }, (table) => [primaryKey({ columns: [
22292
- table.agentName,
22293
- table.provider,
22294
- table.model,
22295
- table.slotKey
22296
- ] })]);
22546
+ ] }), foreignKey({
22547
+ columns: [
22548
+ table.agentName,
22549
+ table.provider,
22550
+ table.model,
22551
+ table.slotKey
22552
+ ],
22553
+ foreignColumns: [
22554
+ daemonSlots.agentName,
22555
+ daemonSlots.provider,
22556
+ daemonSlots.model,
22557
+ daemonSlots.slotKey
22558
+ ]
22559
+ }).onDelete("cascade")]);
22297
22560
  var agentDaemonStateSchema = {
22298
22561
  daemonSlotSessions,
22299
- daemonSlotWorkspaces,
22562
+ daemonWorkspaces,
22300
22563
  daemonSlots
22301
22564
  };
22302
22565
  //#endregion
@@ -22339,59 +22602,7 @@ var SqliteDaemonSlotStore = class {
22339
22602
  this.withSyncDb("initialize sqlite schema", () => {
22340
22603
  this.client.exec("PRAGMA journal_mode = WAL");
22341
22604
  this.client.exec("PRAGMA foreign_keys = ON");
22342
- this.client.exec(`
22343
- CREATE TABLE IF NOT EXISTS daemon_slots (
22344
- agent_name TEXT NOT NULL,
22345
- provider TEXT NOT NULL,
22346
- model TEXT NOT NULL,
22347
- slot_key TEXT NOT NULL,
22348
- task_type TEXT NOT NULL,
22349
- state TEXT NOT NULL CHECK (state IN ('active', 'idle')),
22350
- last_task_id TEXT NOT NULL,
22351
- last_attempt_n INTEGER NOT NULL,
22352
- created_at_ms INTEGER NOT NULL,
22353
- last_used_at_ms INTEGER NOT NULL,
22354
- expires_at_ms INTEGER NOT NULL,
22355
- PRIMARY KEY (agent_name, provider, model, slot_key)
22356
- );
22357
-
22358
- CREATE TABLE IF NOT EXISTS daemon_slot_sessions (
22359
- agent_name TEXT NOT NULL,
22360
- provider TEXT NOT NULL,
22361
- model TEXT NOT NULL,
22362
- slot_key TEXT NOT NULL,
22363
- session_dir TEXT NOT NULL UNIQUE,
22364
- session_path TEXT,
22365
- PRIMARY KEY (agent_name, provider, model, slot_key),
22366
- FOREIGN KEY (agent_name, provider, model, slot_key)
22367
- REFERENCES daemon_slots(agent_name, provider, model, slot_key)
22368
- ON DELETE CASCADE
22369
- );
22370
-
22371
- CREATE TABLE IF NOT EXISTS daemon_slot_workspaces (
22372
- agent_name TEXT NOT NULL,
22373
- provider TEXT NOT NULL,
22374
- model TEXT NOT NULL,
22375
- slot_key TEXT NOT NULL,
22376
- workspace_id TEXT NOT NULL UNIQUE,
22377
- worktree_path TEXT NOT NULL,
22378
- worktree_branch TEXT,
22379
- PRIMARY KEY (agent_name, provider, model, slot_key),
22380
- FOREIGN KEY (agent_name, provider, model, slot_key)
22381
- REFERENCES daemon_slots(agent_name, provider, model, slot_key)
22382
- ON DELETE CASCADE
22383
- );
22384
-
22385
- CREATE INDEX IF NOT EXISTS daemon_slots_expires_idx
22386
- ON daemon_slots (expires_at_ms);
22387
-
22388
- CREATE INDEX IF NOT EXISTS daemon_slots_task_attempt_idx
22389
- ON daemon_slots (
22390
- last_task_id,
22391
- last_attempt_n,
22392
- last_used_at_ms DESC
22393
- );
22394
- `);
22605
+ applySqliteMigrations(this.client);
22395
22606
  });
22396
22607
  } catch (error) {
22397
22608
  throw new DaemonSlotRegistryError("open sqlite database", error);
@@ -22403,34 +22614,45 @@ var SqliteDaemonSlotStore = class {
22403
22614
  async beginSlot(input) {
22404
22615
  const now = Date.now();
22405
22616
  const expiresAtMs = now + input.ttlSec * 1e3;
22406
- await this.withDb("upsert sqlite slot", () => this.db.insert(daemonSlots).values({
22407
- agentName: input.agentName,
22408
- createdAtMs: now,
22409
- expiresAtMs,
22410
- lastAttemptN: input.lastAttemptN,
22411
- lastTaskId: input.lastTaskId,
22412
- lastUsedAtMs: now,
22413
- model: input.model,
22414
- provider: input.provider,
22415
- slotKey: input.slotKey,
22416
- state: "active",
22417
- taskType: input.taskType
22418
- }).onConflictDoUpdate({
22419
- set: {
22420
- expiresAtMs: sql`excluded.expires_at_ms`,
22421
- lastAttemptN: sql`excluded.last_attempt_n`,
22422
- lastTaskId: sql`excluded.last_task_id`,
22423
- lastUsedAtMs: sql`excluded.last_used_at_ms`,
22424
- state: "active",
22425
- taskType: sql`excluded.task_type`
22426
- },
22427
- target: [
22428
- daemonSlots.agentName,
22429
- daemonSlots.provider,
22430
- daemonSlots.model,
22431
- daemonSlots.slotKey
22432
- ]
22433
- }).run());
22617
+ const workspaceId = input.workspaceId !== null && input.worktreePath !== null ? input.workspaceId : null;
22618
+ this.withSyncDb("upsert sqlite slot + workspace", () => {
22619
+ this.client.exec("BEGIN IMMEDIATE");
22620
+ try {
22621
+ const priorWorkspaceId = this.priorSlotWorkspaceId(input);
22622
+ if (workspaceId !== null) this.client.prepare(`INSERT INTO daemon_workspaces
22623
+ (workspace_id, worktree_path, worktree_branch, kind,
22624
+ refcount, created_at_ms, last_used_at_ms)
22625
+ VALUES (?, ?, ?, ?, 0, ?, ?)
22626
+ ON CONFLICT(workspace_id) DO UPDATE SET
22627
+ worktree_path = excluded.worktree_path,
22628
+ worktree_branch = excluded.worktree_branch,
22629
+ last_used_at_ms = excluded.last_used_at_ms`).run(workspaceId, input.worktreePath, input.worktreeBranch, input.workspaceKind ?? "origin", now, now);
22630
+ this.client.prepare(`INSERT INTO daemon_slots
22631
+ (agent_name, provider, model, slot_key, task_type, state,
22632
+ last_task_id, last_attempt_n, workspace_id, created_at_ms,
22633
+ last_used_at_ms, expires_at_ms)
22634
+ VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
22635
+ ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
22636
+ task_type = excluded.task_type,
22637
+ state = 'active',
22638
+ last_task_id = excluded.last_task_id,
22639
+ last_attempt_n = excluded.last_attempt_n,
22640
+ workspace_id = excluded.workspace_id,
22641
+ last_used_at_ms = excluded.last_used_at_ms,
22642
+ expires_at_ms = excluded.expires_at_ms`).run(input.agentName, input.provider, input.model, input.slotKey, input.taskType, input.lastTaskId, input.lastAttemptN, workspaceId, now, now, expiresAtMs);
22643
+ if (priorWorkspaceId !== workspaceId) {
22644
+ if (priorWorkspaceId !== null) this.decrementWorkspaceRefcount(priorWorkspaceId);
22645
+ if (workspaceId !== null) this.client.prepare(`UPDATE daemon_workspaces SET refcount = refcount + 1
22646
+ WHERE workspace_id = ?`).run(workspaceId);
22647
+ }
22648
+ this.client.exec("COMMIT");
22649
+ } catch (error) {
22650
+ try {
22651
+ this.client.exec("ROLLBACK");
22652
+ } catch {}
22653
+ throw error;
22654
+ }
22655
+ });
22434
22656
  const sessionDir = input.sessionDir;
22435
22657
  if (sessionDir !== null) await this.withDb("upsert sqlite slot session", () => this.db.insert(daemonSlotSessions).values({
22436
22658
  agentName: input.agentName,
@@ -22451,29 +22673,17 @@ var SqliteDaemonSlotStore = class {
22451
22673
  daemonSlotSessions.slotKey
22452
22674
  ]
22453
22675
  }).run());
22454
- const workspaceId = input.workspaceId;
22455
- const worktreePath = input.worktreePath;
22456
- if (workspaceId !== null && worktreePath !== null) await this.withDb("upsert sqlite slot workspace", () => this.db.insert(daemonSlotWorkspaces).values({
22457
- agentName: input.agentName,
22458
- model: input.model,
22459
- provider: input.provider,
22460
- slotKey: input.slotKey,
22461
- workspaceId,
22462
- worktreeBranch: input.worktreeBranch,
22463
- worktreePath
22464
- }).onConflictDoUpdate({
22465
- set: {
22466
- workspaceId: sql`excluded.workspace_id`,
22467
- worktreeBranch: sql`excluded.worktree_branch`,
22468
- worktreePath: sql`excluded.worktree_path`
22469
- },
22470
- target: [
22471
- daemonSlotWorkspaces.agentName,
22472
- daemonSlotWorkspaces.provider,
22473
- daemonSlotWorkspaces.model,
22474
- daemonSlotWorkspaces.slotKey
22475
- ]
22476
- }).run());
22676
+ }
22677
+ /** The workspace_id the slot referenced before this begin, or null. */
22678
+ priorSlotWorkspaceId(slot) {
22679
+ return this.client.prepare(`SELECT workspace_id AS workspaceId FROM daemon_slots
22680
+ WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`).get(slot.agentName, slot.provider, slot.model, slot.slotKey)?.workspaceId ?? null;
22681
+ }
22682
+ /** Decrement a workspace refcount; delete the row when it reaches 0. */
22683
+ decrementWorkspaceRefcount(workspaceId) {
22684
+ this.client.prepare(`UPDATE daemon_workspaces SET refcount = refcount - 1
22685
+ WHERE workspace_id = ?`).run(workspaceId);
22686
+ this.client.prepare(`DELETE FROM daemon_workspaces WHERE workspace_id = ? AND refcount <= 0`).run(workspaceId);
22477
22687
  }
22478
22688
  async finishSlot(identity, slotKey, ttlSec, sessionPath) {
22479
22689
  const now = Date.now();
@@ -22502,13 +22712,27 @@ var SqliteDaemonSlotStore = class {
22502
22712
  slot,
22503
22713
  workspace: await this.lookupWorkspace(slot)
22504
22714
  });
22505
- this.withSyncDb("delete expired sqlite slots", () => {
22715
+ const releasable = this.withSyncDb("delete expired sqlite slots", () => {
22506
22716
  this.client.exec("BEGIN IMMEDIATE");
22507
22717
  try {
22508
- for (const slot of slots) this.client.prepare(`DELETE FROM daemon_slots
22509
- WHERE agent_name = ? AND provider = ? AND model = ?
22510
- AND slot_key = ?`).run(slot.agentName, slot.provider, slot.model, slot.slotKey);
22718
+ const orphaned = [];
22719
+ for (const reaped of out) {
22720
+ this.client.prepare(`DELETE FROM daemon_slots
22721
+ WHERE agent_name = ? AND provider = ? AND model = ?
22722
+ AND slot_key = ?`).run(reaped.slot.agentName, reaped.slot.provider, reaped.slot.model, reaped.slot.slotKey);
22723
+ const ws = reaped.workspace;
22724
+ if (ws) {
22725
+ this.client.prepare(`UPDATE daemon_workspaces SET refcount = refcount - 1
22726
+ WHERE workspace_id = ?`).run(ws.workspaceId);
22727
+ const row = this.client.prepare(`SELECT refcount FROM daemon_workspaces WHERE workspace_id = ?`).get(ws.workspaceId);
22728
+ if (!row || row.refcount <= 0) {
22729
+ this.client.prepare(`DELETE FROM daemon_workspaces WHERE workspace_id = ?`).run(ws.workspaceId);
22730
+ orphaned.push(ws);
22731
+ }
22732
+ }
22733
+ }
22511
22734
  this.client.exec("COMMIT");
22735
+ return orphaned;
22512
22736
  } catch (error) {
22513
22737
  try {
22514
22738
  this.client.exec("ROLLBACK");
@@ -22516,7 +22740,9 @@ var SqliteDaemonSlotStore = class {
22516
22740
  throw error;
22517
22741
  }
22518
22742
  });
22519
- return cleanupReapedSlots(out);
22743
+ cleanupReapedSessions(out);
22744
+ cleanupReleasableWorkspaces(releasable);
22745
+ return out;
22520
22746
  } catch (error) {
22521
22747
  throw error instanceof DaemonSlotRegistryError ? error : new DaemonSlotRegistryError("reap expired sqlite slots", error);
22522
22748
  }
@@ -22525,7 +22751,8 @@ var SqliteDaemonSlotStore = class {
22525
22751
  return this.withDb("select sqlite slot session", async () => await this.db.select().from(daemonSlotSessions).where(sqliteSlotSessionIdentityWhere(slot, slot.slotKey)).get() ?? null);
22526
22752
  }
22527
22753
  async lookupWorkspace(slot) {
22528
- return this.withDb("select sqlite slot workspace", async () => await this.db.select().from(daemonSlotWorkspaces).where(sqliteSlotWorkspaceIdentityWhere(slot, slot.slotKey)).get() ?? null);
22754
+ if (!slot.workspaceId) return null;
22755
+ return this.withDb("select sqlite workspace", async () => await this.db.select().from(daemonWorkspaces).where(eq(daemonWorkspaces.workspaceId, slot.workspaceId)).get() ?? null);
22529
22756
  }
22530
22757
  executeProxyQuery(query, params, method) {
22531
22758
  return Promise.resolve(this.withSyncDb("execute sqlite proxy query", () => {
@@ -22575,34 +22802,64 @@ var PgDaemonSlotStore = class {
22575
22802
  await this.ready;
22576
22803
  const now = Date.now();
22577
22804
  const expiresAtMs = now + input.ttlSec * 1e3;
22578
- await this.withDb("upsert postgres slot", () => this.db.insert(pgDaemonSlots).values({
22579
- agentName: input.agentName,
22580
- createdAtMs: now,
22581
- expiresAtMs,
22582
- lastAttemptN: input.lastAttemptN,
22583
- lastTaskId: input.lastTaskId,
22584
- lastUsedAtMs: now,
22585
- model: input.model,
22586
- provider: input.provider,
22587
- slotKey: input.slotKey,
22588
- state: "active",
22589
- taskType: input.taskType
22590
- }).onConflictDoUpdate({
22591
- set: {
22592
- expiresAtMs: sql`excluded.expires_at_ms`,
22593
- lastAttemptN: sql`excluded.last_attempt_n`,
22594
- lastTaskId: sql`excluded.last_task_id`,
22595
- lastUsedAtMs: sql`excluded.last_used_at_ms`,
22805
+ const beginWorkspaceId = input.workspaceId !== null && input.worktreePath !== null ? input.workspaceId : null;
22806
+ await this.withDb("upsert postgres slot + workspace", () => this.db.transaction(async (tx) => {
22807
+ const [priorRow] = await tx.select({ workspaceId: pgDaemonSlots.workspaceId }).from(pgDaemonSlots).where(pgSlotIdentityWhere(input, input.slotKey)).limit(1).execute();
22808
+ const priorWorkspaceId = priorRow?.workspaceId ?? null;
22809
+ if (beginWorkspaceId !== null) await tx.insert(pgDaemonWorkspaces).values({
22810
+ workspaceId: beginWorkspaceId,
22811
+ worktreePath: input.worktreePath,
22812
+ worktreeBranch: input.worktreeBranch,
22813
+ kind: input.workspaceKind ?? "origin",
22814
+ refcount: 0,
22815
+ createdAtMs: now,
22816
+ lastUsedAtMs: now
22817
+ }).onConflictDoUpdate({
22818
+ set: {
22819
+ worktreePath: sql`excluded.worktree_path`,
22820
+ worktreeBranch: sql`excluded.worktree_branch`,
22821
+ lastUsedAtMs: sql`excluded.last_used_at_ms`
22822
+ },
22823
+ target: [pgDaemonWorkspaces.workspaceId]
22824
+ }).execute();
22825
+ await tx.insert(pgDaemonSlots).values({
22826
+ agentName: input.agentName,
22827
+ createdAtMs: now,
22828
+ expiresAtMs,
22829
+ lastAttemptN: input.lastAttemptN,
22830
+ lastTaskId: input.lastTaskId,
22831
+ lastUsedAtMs: now,
22832
+ model: input.model,
22833
+ provider: input.provider,
22834
+ slotKey: input.slotKey,
22596
22835
  state: "active",
22597
- taskType: sql`excluded.task_type`
22598
- },
22599
- target: [
22600
- pgDaemonSlots.agentName,
22601
- pgDaemonSlots.provider,
22602
- pgDaemonSlots.model,
22603
- pgDaemonSlots.slotKey
22604
- ]
22605
- }).execute());
22836
+ taskType: input.taskType,
22837
+ workspaceId: beginWorkspaceId
22838
+ }).onConflictDoUpdate({
22839
+ set: {
22840
+ expiresAtMs: sql`excluded.expires_at_ms`,
22841
+ lastAttemptN: sql`excluded.last_attempt_n`,
22842
+ lastTaskId: sql`excluded.last_task_id`,
22843
+ lastUsedAtMs: sql`excluded.last_used_at_ms`,
22844
+ state: "active",
22845
+ taskType: sql`excluded.task_type`,
22846
+ workspaceId: sql`excluded.workspace_id`
22847
+ },
22848
+ target: [
22849
+ pgDaemonSlots.agentName,
22850
+ pgDaemonSlots.provider,
22851
+ pgDaemonSlots.model,
22852
+ pgDaemonSlots.slotKey
22853
+ ]
22854
+ }).execute();
22855
+ if (priorWorkspaceId !== beginWorkspaceId) {
22856
+ if (priorWorkspaceId !== null) {
22857
+ await tx.update(pgDaemonWorkspaces).set({ refcount: sql`${pgDaemonWorkspaces.refcount} - 1` }).where(eq(pgDaemonWorkspaces.workspaceId, priorWorkspaceId)).execute();
22858
+ await tx.delete(pgDaemonWorkspaces).where(and(eq(pgDaemonWorkspaces.workspaceId, priorWorkspaceId), lte(pgDaemonWorkspaces.refcount, 0))).execute();
22859
+ }
22860
+ if (beginWorkspaceId !== null) await tx.update(pgDaemonWorkspaces).set({ refcount: sql`${pgDaemonWorkspaces.refcount} + 1` }).where(eq(pgDaemonWorkspaces.workspaceId, beginWorkspaceId)).execute();
22861
+ }
22862
+ }));
22606
22863
  const sessionDir = input.sessionDir;
22607
22864
  if (sessionDir !== null) await this.withDb("upsert postgres slot session", () => this.db.insert(pgDaemonSlotSessions).values({
22608
22865
  agentName: input.agentName,
@@ -22623,29 +22880,6 @@ var PgDaemonSlotStore = class {
22623
22880
  pgDaemonSlotSessions.slotKey
22624
22881
  ]
22625
22882
  }).execute());
22626
- const workspaceId = input.workspaceId;
22627
- const worktreePath = input.worktreePath;
22628
- if (workspaceId !== null && worktreePath !== null) await this.withDb("upsert postgres slot workspace", () => this.db.insert(pgDaemonSlotWorkspaces).values({
22629
- agentName: input.agentName,
22630
- model: input.model,
22631
- provider: input.provider,
22632
- slotKey: input.slotKey,
22633
- workspaceId,
22634
- worktreeBranch: input.worktreeBranch,
22635
- worktreePath
22636
- }).onConflictDoUpdate({
22637
- set: {
22638
- workspaceId: sql`excluded.workspace_id`,
22639
- worktreeBranch: sql`excluded.worktree_branch`,
22640
- worktreePath: sql`excluded.worktree_path`
22641
- },
22642
- target: [
22643
- pgDaemonSlotWorkspaces.agentName,
22644
- pgDaemonSlotWorkspaces.provider,
22645
- pgDaemonSlotWorkspaces.model,
22646
- pgDaemonSlotWorkspaces.slotKey
22647
- ]
22648
- }).execute());
22649
22883
  }
22650
22884
  async finishSlot(identity, slotKey, ttlSec, sessionPath) {
22651
22885
  await this.ready;
@@ -22670,91 +22904,50 @@ var PgDaemonSlotStore = class {
22670
22904
  async reapExpiredSlots(now = Date.now()) {
22671
22905
  await this.ready;
22672
22906
  try {
22673
- return cleanupReapedSlots(await this.db.transaction(async (tx) => {
22907
+ const { out, releasable } = await this.db.transaction(async (tx) => {
22674
22908
  const slots = await tx.select().from(pgDaemonSlots).where(lte(pgDaemonSlots.expiresAtMs, now)).execute();
22675
22909
  const reaped = [];
22910
+ const orphaned = [];
22676
22911
  for (const slot of slots) {
22677
22912
  const [session = null] = await tx.select().from(pgDaemonSlotSessions).where(pgSlotSessionIdentityWhere(slot, slot.slotKey)).limit(1).execute();
22678
- const [workspace = null] = await tx.select().from(pgDaemonSlotWorkspaces).where(pgSlotWorkspaceIdentityWhere(slot, slot.slotKey)).limit(1).execute();
22913
+ const workspace = slot.workspaceId ? (await tx.select().from(pgDaemonWorkspaces).where(eq(pgDaemonWorkspaces.workspaceId, slot.workspaceId)).limit(1).execute())[0] ?? null : null;
22679
22914
  reaped.push({
22680
22915
  session,
22681
22916
  slot,
22682
22917
  workspace
22683
22918
  });
22684
22919
  await tx.delete(pgDaemonSlots).where(pgSlotIdentityWhere(slot, slot.slotKey)).execute();
22920
+ if (workspace) {
22921
+ await tx.update(pgDaemonWorkspaces).set({ refcount: sql`${pgDaemonWorkspaces.refcount} - 1` }).where(eq(pgDaemonWorkspaces.workspaceId, workspace.workspaceId)).execute();
22922
+ const [after] = await tx.select({ refcount: pgDaemonWorkspaces.refcount }).from(pgDaemonWorkspaces).where(eq(pgDaemonWorkspaces.workspaceId, workspace.workspaceId)).limit(1).execute();
22923
+ if (!after || after.refcount <= 0) {
22924
+ await tx.delete(pgDaemonWorkspaces).where(eq(pgDaemonWorkspaces.workspaceId, workspace.workspaceId)).execute();
22925
+ orphaned.push(workspace);
22926
+ }
22927
+ }
22685
22928
  }
22686
- return reaped;
22687
- }));
22929
+ return {
22930
+ out: reaped,
22931
+ releasable: orphaned
22932
+ };
22933
+ });
22934
+ cleanupReapedSessions(out);
22935
+ cleanupReleasableWorkspaces(releasable);
22936
+ return out;
22688
22937
  } catch (error) {
22689
22938
  throw error instanceof DaemonSlotRegistryError ? error : new DaemonSlotRegistryError("reap expired postgres slots", error);
22690
22939
  }
22691
22940
  }
22692
22941
  async initialize() {
22693
- await this.withDb("initialize postgres schema", () => this.pool.query(`
22694
- CREATE TABLE IF NOT EXISTS daemon_slots (
22695
- agent_name TEXT NOT NULL,
22696
- provider TEXT NOT NULL,
22697
- model TEXT NOT NULL,
22698
- slot_key TEXT NOT NULL,
22699
- task_type TEXT NOT NULL,
22700
- state TEXT NOT NULL CHECK (state IN ('active', 'idle')),
22701
- last_task_id TEXT NOT NULL,
22702
- last_attempt_n INTEGER NOT NULL,
22703
- created_at_ms BIGINT NOT NULL,
22704
- last_used_at_ms BIGINT NOT NULL,
22705
- expires_at_ms BIGINT NOT NULL,
22706
- PRIMARY KEY (agent_name, provider, model, slot_key)
22707
- );
22708
-
22709
- CREATE TABLE IF NOT EXISTS daemon_slot_sessions (
22710
- agent_name TEXT NOT NULL,
22711
- provider TEXT NOT NULL,
22712
- model TEXT NOT NULL,
22713
- slot_key TEXT NOT NULL,
22714
- session_dir TEXT NOT NULL UNIQUE,
22715
- session_path TEXT,
22716
- PRIMARY KEY (agent_name, provider, model, slot_key),
22717
- FOREIGN KEY (agent_name, provider, model, slot_key)
22718
- REFERENCES daemon_slots(agent_name, provider, model, slot_key)
22719
- ON DELETE CASCADE
22720
- );
22721
-
22722
- CREATE TABLE IF NOT EXISTS daemon_slot_workspaces (
22723
- agent_name TEXT NOT NULL,
22724
- provider TEXT NOT NULL,
22725
- model TEXT NOT NULL,
22726
- slot_key TEXT NOT NULL,
22727
- workspace_id TEXT NOT NULL UNIQUE,
22728
- worktree_path TEXT NOT NULL,
22729
- worktree_branch TEXT,
22730
- PRIMARY KEY (agent_name, provider, model, slot_key),
22731
- FOREIGN KEY (agent_name, provider, model, slot_key)
22732
- REFERENCES daemon_slots(agent_name, provider, model, slot_key)
22733
- ON DELETE CASCADE
22734
- );
22735
-
22736
- CREATE INDEX IF NOT EXISTS daemon_slots_expires_idx
22737
- ON daemon_slots (expires_at_ms);
22738
-
22739
- CREATE INDEX IF NOT EXISTS daemon_slots_task_attempt_idx
22740
- ON daemon_slots (
22741
- last_task_id,
22742
- last_attempt_n,
22743
- last_used_at_ms DESC
22744
- );
22745
-
22746
- ALTER TABLE daemon_slots
22747
- ALTER COLUMN created_at_ms TYPE BIGINT,
22748
- ALTER COLUMN last_used_at_ms TYPE BIGINT,
22749
- ALTER COLUMN expires_at_ms TYPE BIGINT;
22750
- `));
22942
+ await this.withDb("initialize postgres schema", () => runPgMigrations(this.pool));
22751
22943
  }
22752
22944
  async lookupSession(slot) {
22753
22945
  const [session] = await this.withDb("select postgres slot session", () => this.db.select().from(pgDaemonSlotSessions).where(pgSlotSessionIdentityWhere(slot, slot.slotKey)).limit(1).execute());
22754
22946
  return session ?? null;
22755
22947
  }
22756
22948
  async lookupWorkspace(slot) {
22757
- const [workspace] = await this.withDb("select postgres slot workspace", () => this.db.select().from(pgDaemonSlotWorkspaces).where(pgSlotWorkspaceIdentityWhere(slot, slot.slotKey)).limit(1).execute());
22949
+ if (!slot.workspaceId) return null;
22950
+ const [workspace] = await this.withDb("select postgres workspace", () => this.db.select().from(pgDaemonWorkspaces).where(eq(pgDaemonWorkspaces.workspaceId, slot.workspaceId)).limit(1).execute());
22758
22951
  return workspace ?? null;
22759
22952
  }
22760
22953
  async withDb(operation, fn) {
@@ -22794,24 +22987,27 @@ function sqliteSlotIdentityWhere(identity, slotKey) {
22794
22987
  function sqliteSlotSessionIdentityWhere(identity, slotKey) {
22795
22988
  return and(eq(daemonSlotSessions.agentName, identity.agentName), eq(daemonSlotSessions.provider, identity.provider), eq(daemonSlotSessions.model, identity.model), eq(daemonSlotSessions.slotKey, slotKey));
22796
22989
  }
22797
- function sqliteSlotWorkspaceIdentityWhere(identity, slotKey) {
22798
- return and(eq(daemonSlotWorkspaces.agentName, identity.agentName), eq(daemonSlotWorkspaces.provider, identity.provider), eq(daemonSlotWorkspaces.model, identity.model), eq(daemonSlotWorkspaces.slotKey, slotKey));
22799
- }
22800
22990
  function pgSlotIdentityWhere(identity, slotKey) {
22801
22991
  return and(eq(pgDaemonSlots.agentName, identity.agentName), eq(pgDaemonSlots.provider, identity.provider), eq(pgDaemonSlots.model, identity.model), eq(pgDaemonSlots.slotKey, slotKey));
22802
22992
  }
22803
22993
  function pgSlotSessionIdentityWhere(identity, slotKey) {
22804
22994
  return and(eq(pgDaemonSlotSessions.agentName, identity.agentName), eq(pgDaemonSlotSessions.provider, identity.provider), eq(pgDaemonSlotSessions.model, identity.model), eq(pgDaemonSlotSessions.slotKey, slotKey));
22805
22995
  }
22806
- function pgSlotWorkspaceIdentityWhere(identity, slotKey) {
22807
- return and(eq(pgDaemonSlotWorkspaces.agentName, identity.agentName), eq(pgDaemonSlotWorkspaces.provider, identity.provider), eq(pgDaemonSlotWorkspaces.model, identity.model), eq(pgDaemonSlotWorkspaces.slotKey, slotKey));
22996
+ /** Remove the persisted Pi session dir for each reaped slot. */
22997
+ function cleanupReapedSessions(slots) {
22998
+ for (const item of slots) if (item.session) cleanupPiSessionDir(item.session.sessionDir);
22808
22999
  }
22809
- function cleanupReapedSlots(slots) {
22810
- for (const item of slots) {
22811
- if (item.session) cleanupPiSessionDir(item.session.sessionDir);
22812
- if (item.workspace) cleanupReusableWorktree(item.workspace.worktreePath);
22813
- }
22814
- return slots;
23000
+ /**
23001
+ * Remove on-disk artifacts for workspaces whose refcount reached 0. Worktrees
23002
+ * (origin/fork) are removed via `git worktree remove`; scratch workspaces are
23003
+ * plain directories removed via rm.
23004
+ */
23005
+ function cleanupReleasableWorkspaces(workspaces) {
23006
+ for (const ws of workspaces) if (ws.kind === "scratch") rmSync(ws.worktreePath, {
23007
+ recursive: true,
23008
+ force: true
23009
+ });
23010
+ else cleanupReusableWorktree(ws.worktreePath);
22815
23011
  }
22816
23012
  function cleanupPiSessionDir(sessionDir) {
22817
23013
  rmSync(sessionDir, {
@@ -24323,7 +24519,7 @@ function buildRunEvalUserPrompt(input, ctx) {
24323
24519
  function buildTaskUserPrompt(task, ctx) {
24324
24520
  switch (task.taskType) {
24325
24521
  case FREEFORM_TYPE:
24326
- if (!Check(FreeformInput, task.input)) {
24522
+ if (!Check$2(FreeformInput, task.input)) {
24327
24523
  const errors = [...Errors(FreeformInput, task.input)];
24328
24524
  throw new Error(`freeform input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24329
24525
  }
@@ -24332,7 +24528,7 @@ function buildTaskUserPrompt(task, ctx) {
24332
24528
  priorContext: ctx.priorContext
24333
24529
  });
24334
24530
  case FULFILL_BRIEF_TYPE:
24335
- if (!Check(FulfillBriefInput, task.input)) {
24531
+ if (!Check$2(FulfillBriefInput, task.input)) {
24336
24532
  const errors = [...Errors(FulfillBriefInput, task.input)];
24337
24533
  throw new Error(`fulfill_brief input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24338
24534
  }
@@ -24343,7 +24539,7 @@ function buildTaskUserPrompt(task, ctx) {
24343
24539
  workspace: ctx.workspace
24344
24540
  });
24345
24541
  case ASSESS_BRIEF_TYPE:
24346
- if (!Check(AssessBriefInput, task.input)) {
24542
+ if (!Check$2(AssessBriefInput, task.input)) {
24347
24543
  const errors = [...Errors(AssessBriefInput, task.input)];
24348
24544
  throw new Error(`assess_brief input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24349
24545
  }
@@ -24353,7 +24549,7 @@ function buildTaskUserPrompt(task, ctx) {
24353
24549
  workspace: ctx.workspace
24354
24550
  });
24355
24551
  case CURATE_PACK_TYPE:
24356
- if (!Check(CuratePackInput, task.input)) {
24552
+ if (!Check$2(CuratePackInput, task.input)) {
24357
24553
  const errors = [...Errors(CuratePackInput, task.input)];
24358
24554
  throw new Error(`curate_pack input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24359
24555
  }
@@ -24362,7 +24558,7 @@ function buildTaskUserPrompt(task, ctx) {
24362
24558
  taskId: ctx.taskId
24363
24559
  });
24364
24560
  case RENDER_PACK_TYPE:
24365
- if (!Check(RenderPackInput, task.input)) {
24561
+ if (!Check$2(RenderPackInput, task.input)) {
24366
24562
  const errors = [...Errors(RenderPackInput, task.input)];
24367
24563
  throw new Error(`render_pack input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24368
24564
  }
@@ -24371,7 +24567,7 @@ function buildTaskUserPrompt(task, ctx) {
24371
24567
  taskId: ctx.taskId
24372
24568
  });
24373
24569
  case JUDGE_PACK_TYPE:
24374
- if (!Check(JudgePackInput, task.input)) {
24570
+ if (!Check$2(JudgePackInput, task.input)) {
24375
24571
  const errors = [...Errors(JudgePackInput, task.input)];
24376
24572
  throw new Error(`judge_pack input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24377
24573
  }
@@ -24380,7 +24576,7 @@ function buildTaskUserPrompt(task, ctx) {
24380
24576
  taskId: ctx.taskId
24381
24577
  });
24382
24578
  case JUDGE_EVAL_ATTEMPT_TYPE:
24383
- if (!Check(JudgeEvalAttemptInput, task.input)) {
24579
+ if (!Check$2(JudgeEvalAttemptInput, task.input)) {
24384
24580
  const errors = [...Errors(JudgeEvalAttemptInput, task.input)];
24385
24581
  throw new Error(`judge_eval_attempt input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24386
24582
  }
@@ -24390,7 +24586,7 @@ function buildTaskUserPrompt(task, ctx) {
24390
24586
  workspace: ctx.workspace
24391
24587
  });
24392
24588
  case PR_REVIEW_TYPE:
24393
- if (!Check(PrReviewInput, task.input)) {
24589
+ if (!Check$2(PrReviewInput, task.input)) {
24394
24590
  const errors = [...Errors(PrReviewInput, task.input)];
24395
24591
  throw new Error(`pr_review input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24396
24592
  }
@@ -24400,7 +24596,7 @@ function buildTaskUserPrompt(task, ctx) {
24400
24596
  workspace: ctx.workspace
24401
24597
  });
24402
24598
  case RUN_EVAL_TYPE:
24403
- if (!Check(RunEvalInput, task.input)) {
24599
+ if (!Check$2(RunEvalInput, task.input)) {
24404
24600
  const errors = [...Errors(RunEvalInput, task.input)];
24405
24601
  throw new Error(`run_eval input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
24406
24602
  }
@@ -32845,8 +33041,7 @@ async function resumeVm(config) {
32845
33041
  signal: config.signal
32846
33042
  });
32847
33043
  if (creds.gitconfig) {
32848
- const vmSigningKey = `${vmSshDir}/id_ed25519`;
32849
- const vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
33044
+ const vmGitconfig = rewriteGitconfigPaths(creds.gitconfig, vmSshDir, vmAgentDir);
32850
33045
  await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, {
32851
33046
  mode: 420,
32852
33047
  signal: config.signal
@@ -32869,17 +33064,6 @@ async function resumeVm(config) {
32869
33064
  signal: config.signal
32870
33065
  });
32871
33066
  await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet", { signal: config.signal });
32872
- const gitCredHelperPath = `${vmSshDir}/git-credential-moltnet`;
32873
- const credHelperScript = `#!/bin/sh
32874
- echo "username=x-access-token"
32875
- echo "password=$(moltnet github token --credentials ${vmSshDir}/moltnet.json)"
32876
- `;
32877
- await vm.fs.writeFile(gitCredHelperPath, credHelperScript, {
32878
- mode: 493,
32879
- signal: config.signal
32880
- });
32881
- await vmRun(vm, "git credential helper", `git config --global credential.helper ${gitCredHelperPath} && \
32882
- git config --global url."https://github.com/".insteadOf "git@github.com:"`, config.signal);
32883
33067
  return {
32884
33068
  vm,
32885
33069
  credentials: creds,
@@ -32898,6 +33082,30 @@ echo "password=$(moltnet github token --credentials ${vmSshDir}/moltnet.json)"
32898
33082
  }
32899
33083
  }
32900
33084
  /**
33085
+ * Rewrite host-absolute paths inside an agent gitconfig to VM-local
33086
+ * equivalents before injecting it into the guest.
33087
+ *
33088
+ * Two rewrites:
33089
+ * - `signingKey = <host path>` → `<vmSshDir>/id_ed25519`
33090
+ * - `... credential-helper --credentials <host moltnet.json>`
33091
+ * → `<vmAgentDir>/moltnet.json`
33092
+ *
33093
+ * The credential-helper line is generated host-side by `moltnet github setup`
33094
+ * with a host-absolute `--credentials` path; inside the guest that path is
33095
+ * invalid, so it must point at the VM-side moltnet.json. The `insteadOf`
33096
+ * rewrite rule and every other line are workspace-independent and pass through
33097
+ * unchanged. A gitconfig without a credential helper is rewritten only for
33098
+ * `signingKey`.
33099
+ *
33100
+ * This is the single source of truth for git push auth in the guest: the
33101
+ * injected gitconfig carries the tokenless mint-on-demand helper, so the VM
33102
+ * no longer hand-rolls a credential-helper script or runs an imperative
33103
+ * `git config --global ... insteadOf` against the guest $HOME.
33104
+ */
33105
+ function rewriteGitconfigPaths(gitconfig, vmSshDir, vmAgentDir) {
33106
+ return gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSshDir}/id_ed25519`).replace(/(moltnet github credential-helper --credentials )\S+/g, `$1${vmAgentDir}/moltnet.json`);
33107
+ }
33108
+ /**
32901
33109
  * Rewrite host-absolute paths inside moltnet.json to VM-local equivalents.
32902
33110
  *
32903
33111
  * Fields rewritten:
@@ -33458,7 +33666,7 @@ function createSubagentTool(args) {
33458
33666
  description: subagentToolDescription(),
33459
33667
  parameters: SubagentToolParameters,
33460
33668
  async execute(_id, params) {
33461
- if (!Check(SubagentToolParameters, params)) return toolError(`subagent: invalid parameters: ${JSON.stringify([...Errors(SubagentToolParameters, params)].slice(0, 3))}`);
33669
+ if (!Check$2(SubagentToolParameters, params)) return toolError(`subagent: invalid parameters: ${JSON.stringify([...Errors(SubagentToolParameters, params)].slice(0, 3))}`);
33462
33670
  const { task, output_schema } = params;
33463
33671
  const contract = contractRegistry.get(output_schema);
33464
33672
  if (!contract) return toolError(`subagent: unknown output_schema "${output_schema}". Registered contracts: [${contractRegistry.list().map((c) => c.name).join(", ")}]`);
@@ -33471,7 +33679,7 @@ function createSubagentTool(args) {
33471
33679
  description: `Submit your structured output for this subagent task. Call exactly once when done. Args MUST match the ${output_schema} contract; mismatches return a tool error you can recover from in the same session.`,
33472
33680
  parameters: contract.parametersSchema,
33473
33681
  async execute(_innerId, innerParams) {
33474
- if (!Check(contract.parametersSchema, innerParams)) return toolError(`submit_subagent_output: schema validation failed: ${[...Errors(contract.parametersSchema, innerParams)].slice(0, 3).map((e) => `${e.instancePath}: ${e.message}`).join("; ")}. Re-call with a corrected payload.`);
33682
+ if (!Check$2(contract.parametersSchema, innerParams)) return toolError(`submit_subagent_output: schema validation failed: ${[...Errors(contract.parametersSchema, innerParams)].slice(0, 3).map((e) => `${e.instancePath}: ${e.message}`).join("; ")}. Re-call with a corrected payload.`);
33475
33683
  captured = innerParams;
33476
33684
  return {
33477
33685
  content: [{
@@ -33877,10 +34085,11 @@ function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
33877
34085
  const relMount = relative(mainRepo, requestedMountPath);
33878
34086
  const cwdPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
33879
34087
  const keepWorkspace = executionPlan?.workspaceScope === "session" && executionPlan.sessionKey !== null;
33880
- if (keepWorkspace) ensureReusableTaskWorktree(mainRepo, worktreeDir, branch);
34088
+ const baseRefOverride = executionPlan?.worktreeBaseRef ?? null;
34089
+ if (keepWorkspace) ensureReusableTaskWorktree(mainRepo, worktreeDir, branch, baseRefOverride);
33881
34090
  else {
33882
34091
  removeExistingTaskWorktree(mainRepo, worktreeDir);
33883
- addTaskWorktree(mainRepo, worktreeDir, branch);
34092
+ addTaskWorktree(mainRepo, worktreeDir, branch, baseRefOverride);
33884
34093
  }
33885
34094
  return {
33886
34095
  mountPath: mainRepo,
@@ -33905,14 +34114,15 @@ function resolveTaskWorktreePath(mainRepo, workspaceId) {
33905
34114
  function resolveTaskScratchPath(mainRepo, workspaceId) {
33906
34115
  return join(mainRepo, ".moltnet", "d", "task-workspaces", workspaceId);
33907
34116
  }
33908
- function ensureReusableTaskWorktree(mainRepo, worktreeDir, branch) {
34117
+ function ensureReusableTaskWorktree(mainRepo, worktreeDir, branch, baseRefOverride = null) {
33909
34118
  if (isRegisteredWorktree(mainRepo, worktreeDir)) return;
33910
34119
  if (existsSync(worktreeDir)) throw new Error(`Expected reusable worktree ${worktreeDir} to be git-managed, but it exists outside git worktree metadata.`);
33911
- addTaskWorktree(mainRepo, worktreeDir, branch);
34120
+ addTaskWorktree(mainRepo, worktreeDir, branch, baseRefOverride);
33912
34121
  }
33913
- function addTaskWorktree(mainRepo, worktreeDir, branch) {
33914
- const baseRef = resolveWorktreeBaseRef(mainRepo);
33915
- execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
34122
+ function addTaskWorktree(mainRepo, worktreeDir, branch, baseRefOverride = null) {
34123
+ const branchExists = gitRefExists(mainRepo, `refs/heads/${branch}`);
34124
+ const baseRef = baseRefOverride ?? resolveWorktreeBaseRef(mainRepo);
34125
+ execFileSync("git", branchExists ? [
33916
34126
  "-C",
33917
34127
  mainRepo,
33918
34128
  "worktree",
@@ -34295,7 +34505,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34295
34505
  let injectedContext;
34296
34506
  try {
34297
34507
  const contextArray = rawContext === void 0 ? [] : rawContext;
34298
- if (!Check(TaskContext, contextArray)) throw new Error(`task.input.context failed TaskContext validation: ${JSON.stringify([...Errors(TaskContext, contextArray)].slice(0, 3))}`);
34508
+ if (!Check$2(TaskContext, contextArray)) throw new Error(`task.input.context failed TaskContext validation: ${JSON.stringify([...Errors(TaskContext, contextArray)].slice(0, 3))}`);
34299
34509
  injectedContext = await injectTaskContext({
34300
34510
  context: contextArray,
34301
34511
  fs: managed.vm.fs,
@@ -35074,13 +35284,32 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
35074
35284
  const resolution = await resolveWarmSlot(slotRegistry, continueFrom.taskId, continueFrom.attemptN, stateDirs);
35075
35285
  if (resolution.kind === "missing") throw new ProducerContextResolutionError(`Continuation source task ${continueFrom.taskId} attempt ${continueFrom.attemptN} has no live daemon slot on this daemon — claim affinity filter should have prevented this claim`);
35076
35286
  if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Continuation source attempt ${continueFrom.taskId}/${continueFrom.attemptN} has no persisted Pi session path`);
35287
+ const sessionDir = `${stateDirs.piSessionsDir}/continue-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
35288
+ const parentBranch = resolution.producerSlot.workspace?.worktreeBranch ?? null;
35289
+ if (continueFrom.mode === "fork") {
35290
+ if (!parentBranch) throw new ProducerContextResolutionError(`Cannot fork continuation of ${continueFrom.taskId}/${continueFrom.attemptN}: producer slot has no worktree branch to fork from`);
35291
+ const forkWorkspaceId = `fork-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
35292
+ const forkBranch = `${parentBranch}-fork-${claimedTask.task.id.slice(0, 8)}-${claimedTask.attemptN}`;
35293
+ return {
35294
+ ...basePlan,
35295
+ workspaceMode: "dedicated_worktree",
35296
+ workspaceId: forkWorkspaceId,
35297
+ worktreeBranch: forkBranch,
35298
+ worktreeBaseRef: parentBranch,
35299
+ workspaceKind: "fork",
35300
+ sessionPersistence: {
35301
+ sessionDir,
35302
+ forkFromSessionPath: resolution.sessionPath
35303
+ }
35304
+ };
35305
+ }
35077
35306
  return {
35078
35307
  ...basePlan,
35079
35308
  workspaceMode: "dedicated_worktree",
35080
35309
  workspaceId: resolution.producerSlot.workspace?.workspaceId ?? null,
35081
- worktreeBranch: resolution.producerSlot.workspace?.worktreeBranch ?? null,
35310
+ worktreeBranch: parentBranch,
35082
35311
  sessionPersistence: {
35083
- sessionDir: `${stateDirs.piSessionsDir}/continue-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`,
35312
+ sessionDir,
35084
35313
  forkFromSessionPath: resolution.sessionPath
35085
35314
  }
35086
35315
  };
@@ -35096,6 +35325,7 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
35096
35325
  ...basePlan,
35097
35326
  workspaceMode: "scratch_mount",
35098
35327
  worktreeBranch: null,
35328
+ workspaceKind: "scratch",
35099
35329
  workspaceSeed: {
35100
35330
  copyFromPath: resolution.workspacePath,
35101
35331
  source: "producer"
@@ -35900,6 +36130,7 @@ async function runPolling(opts) {
35900
36130
  workspaceId: executionPlan.workspaceId,
35901
36131
  worktreePath: resolveRecordedWorkspacePath$1(mainRepo, stateDirs.rootDir, executionPlan),
35902
36132
  worktreeBranch: executionPlan.worktreeBranch,
36133
+ workspaceKind: executionPlan.workspaceKind,
35903
36134
  lastTaskId: claimedTask.task.id,
35904
36135
  lastAttemptN: claimedTask.attemptN,
35905
36136
  ttlSec: common.warmSessionTtlSec
@@ -36158,6 +36389,7 @@ async function runOnce(argv) {
36158
36389
  workspaceId: executionPlan.workspaceId,
36159
36390
  worktreePath: resolveRecordedWorkspacePath(mainRepo, stateDirs.rootDir, executionPlan),
36160
36391
  worktreeBranch: executionPlan.worktreeBranch,
36392
+ workspaceKind: executionPlan.workspaceKind,
36161
36393
  lastTaskId: claimedTask.task.id,
36162
36394
  lastAttemptN: claimedTask.attemptN,
36163
36395
  ttlSec: opts.warmSessionTtlSec