opencode-supertask 0.1.31 → 0.1.32-rc.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.
@@ -1,27 +1,4 @@
1
- interface GatewayConfig {
2
- configVersion: 2;
3
- worker: {
4
- maxConcurrency: number;
5
- pollIntervalMs: number;
6
- heartbeatIntervalMs: number;
7
- taskTimeoutMs: number;
8
- shutdownGracePeriodMs: number;
9
- };
10
- scheduler: {
11
- enabled: boolean;
12
- checkIntervalMs: number;
13
- };
14
- watchdog: {
15
- heartbeatTimeoutMs: number;
16
- checkIntervalMs: number;
17
- cleanupIntervalMs: number;
18
- retentionDays: number;
19
- };
20
- dashboard: {
21
- enabled: boolean;
22
- port: number;
23
- };
24
- }
1
+ import { G as GatewayConfig } from '../config-CCUuD6Az.js';
25
2
 
26
3
  interface WorkerEngineOptions {
27
4
  opencodeBin?: string;
@@ -5359,6 +5359,42 @@ function computeBackoff(retryCount, baseMs = 3e4, maxMs = MAX_BACKOFF_MS) {
5359
5359
  return Math.min(baseMs * Math.pow(2, retryCount - 1), maxMs);
5360
5360
  }
5361
5361
 
5362
+ // src/core/task-working-directory.ts
5363
+ import { statSync } from "fs";
5364
+ import { isAbsolute } from "path";
5365
+ var InvalidTaskWorkingDirectoryError = class extends Error {
5366
+ constructor(message) {
5367
+ super(message);
5368
+ this.name = "InvalidTaskWorkingDirectoryError";
5369
+ }
5370
+ };
5371
+ function validateTaskWorkingDirectory(cwd) {
5372
+ if (cwd == null) return;
5373
+ if (!cwd.trim()) {
5374
+ throw new InvalidTaskWorkingDirectoryError("\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
5375
+ }
5376
+ if (!isAbsolute(cwd)) {
5377
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${cwd}`);
5378
+ }
5379
+ let stat;
5380
+ try {
5381
+ stat = statSync(cwd);
5382
+ } catch (error) {
5383
+ const detail = error instanceof Error ? error.message : String(error);
5384
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${cwd}\uFF08${detail}\uFF09`);
5385
+ }
5386
+ if (!stat.isDirectory()) {
5387
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55\uFF1A${cwd}`);
5388
+ }
5389
+ }
5390
+
5391
+ // src/core/task-batch.ts
5392
+ var TASK_BATCH_TRIM_CHARACTERS = " \n\v\f\r\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF";
5393
+ function normalizeTaskBatchId(batchId) {
5394
+ if (batchId == null) return batchId;
5395
+ return batchId.trim() || null;
5396
+ }
5397
+
5362
5398
  // src/core/services/task.service.ts
5363
5399
  var { tasks: tasks2, taskRuns: taskRuns2 } = schema_exports;
5364
5400
  var cleanupInvocation = 0;
@@ -5422,34 +5458,75 @@ var TaskService = class {
5422
5458
  return conditions;
5423
5459
  }
5424
5460
  static async add(data) {
5425
- this.validateNewTask(data);
5461
+ const normalizedData = {
5462
+ ...data,
5463
+ batchId: normalizeTaskBatchId(data.batchId)
5464
+ };
5465
+ this.validateNewTask(normalizedData);
5426
5466
  return db.transaction((tx) => {
5427
- if (data.dependsOn != null) {
5467
+ if (normalizedData.dependsOn != null) {
5428
5468
  const dependency = tx.select({
5429
5469
  id: tasks2.id,
5430
5470
  cwd: tasks2.cwd,
5431
5471
  status: tasks2.status,
5432
5472
  retryCount: tasks2.retryCount,
5433
5473
  maxRetries: tasks2.maxRetries
5434
- }).from(tasks2).where(eq(tasks2.id, data.dependsOn)).get();
5474
+ }).from(tasks2).where(eq(tasks2.id, normalizedData.dependsOn)).get();
5435
5475
  if (!dependency) {
5436
- throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${data.dependsOn} \u4E0D\u5B58\u5728`);
5476
+ throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${normalizedData.dependsOn} \u4E0D\u5B58\u5728`);
5437
5477
  }
5438
- if ((dependency.cwd ?? null) !== (data.cwd ?? null)) {
5478
+ if ((dependency.cwd ?? null) !== (normalizedData.cwd ?? null)) {
5439
5479
  throw new Error("dependsOn \u5FC5\u987B\u6307\u5411\u540C\u4E00 cwd \u7684\u4EFB\u52A1");
5440
5480
  }
5441
5481
  const dependencyIsRecoverable = dependency.status === "pending" || dependency.status === "running" || dependency.status === "done" || dependency.status === "failed" && (dependency.retryCount ?? 0) <= (dependency.maxRetries ?? 3);
5442
5482
  if (!dependencyIsRecoverable) {
5443
- throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${data.dependsOn} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`);
5483
+ throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${normalizedData.dependsOn} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`);
5444
5484
  }
5445
5485
  }
5446
- return tx.insert(tasks2).values(data).returning().get();
5486
+ return tx.insert(tasks2).values(normalizedData).returning().get();
5487
+ }, { behavior: "immediate" });
5488
+ }
5489
+ static async update(id, data, scope = {}) {
5490
+ if (Object.keys(data).length === 0) throw new Error("\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u8981\u4FEE\u6539\u7684\u5B57\u6BB5");
5491
+ const normalizedData = data.batchId === void 0 ? data : { ...data, batchId: normalizeTaskBatchId(data.batchId) ?? null };
5492
+ return db.transaction((tx) => {
5493
+ const task = tx.select().from(tasks2).where(and(
5494
+ eq(tasks2.id, id),
5495
+ sql`${tasks2.status} IN ('pending', 'failed', 'dead_letter')`,
5496
+ ...this.buildScopeWhere(scope)
5497
+ )).get();
5498
+ if (!task) return null;
5499
+ this.validateNewTask({
5500
+ name: normalizedData.name ?? task.name,
5501
+ agent: normalizedData.agent ?? task.agent,
5502
+ model: normalizedData.model ?? task.model,
5503
+ prompt: normalizedData.prompt ?? task.prompt,
5504
+ cwd: task.cwd,
5505
+ category: normalizedData.category ?? task.category,
5506
+ importance: normalizedData.importance ?? task.importance,
5507
+ urgency: normalizedData.urgency ?? task.urgency,
5508
+ batchId: normalizedData.batchId === void 0 ? task.batchId : normalizedData.batchId,
5509
+ maxRetries: normalizedData.maxRetries ?? task.maxRetries,
5510
+ retryBackoffMs: normalizedData.retryBackoffMs ?? task.retryBackoffMs,
5511
+ timeoutMs: normalizedData.timeoutMs === void 0 ? task.timeoutMs : normalizedData.timeoutMs,
5512
+ dependsOn: task.dependsOn
5513
+ });
5514
+ const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
5515
+ const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
5516
+ return tx.update(tasks2).set({
5517
+ ...normalizedData,
5518
+ ...exhausted ? {
5519
+ status: "dead_letter",
5520
+ retryAfter: null
5521
+ } : {}
5522
+ }).where(eq(tasks2.id, id)).returning().get() ?? null;
5447
5523
  }, { behavior: "immediate" });
5448
5524
  }
5449
5525
  static validateNewTask(data) {
5450
5526
  if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
5451
5527
  if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
5452
5528
  if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
5529
+ validateTaskWorkingDirectory(data.cwd);
5453
5530
  this.validateInteger("importance", data.importance, 1, 5);
5454
5531
  this.validateInteger("urgency", data.urgency, 1, 5);
5455
5532
  this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
@@ -5470,12 +5547,14 @@ var TaskService = class {
5470
5547
  isNull(tasks2.retryAfter),
5471
5548
  sql`${tasks2.retryAfter} <= ${nowMs}`
5472
5549
  );
5473
- const hasExcludedBatches = scope.excludedBatchIds && scope.excludedBatchIds.length > 0;
5550
+ const excludedBatchIds = [...new Set((scope.excludedBatchIds ?? []).map((batchId) => normalizeTaskBatchId(batchId)).filter((batchId) => Boolean(batchId)))];
5551
+ const hasExcludedBatches = excludedBatchIds.length > 0;
5474
5552
  let batchFilter;
5475
5553
  if (hasExcludedBatches) {
5476
5554
  batchFilter = or(
5477
5555
  isNull(tasks2.batchId),
5478
- sql`${tasks2.batchId} NOT IN ${scope.excludedBatchIds}`
5556
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
5557
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) NOT IN ${excludedBatchIds}`
5479
5558
  );
5480
5559
  }
5481
5560
  const statusConditions = or(
@@ -5509,9 +5588,11 @@ var TaskService = class {
5509
5588
  ),
5510
5589
  or(
5511
5590
  isNull(tasks2.batchId),
5591
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
5512
5592
  sql`NOT EXISTS (
5513
5593
  SELECT 1 FROM tasks AS running_batch_task
5514
- WHERE running_batch_task.batch_id = ${tasks2.batchId}
5594
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
5595
+ = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
5515
5596
  AND (
5516
5597
  running_batch_task.status = 'running'
5517
5598
  OR EXISTS (
@@ -5535,12 +5616,18 @@ var TaskService = class {
5535
5616
  ).limit(1);
5536
5617
  return result[0] ?? null;
5537
5618
  }
5538
- static async countRunning() {
5619
+ static async countRunning(scope = {}) {
5620
+ const scopeConditions = scope.legacyCwd ? [sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`] : this.buildScopeWhere(scope);
5621
+ if (scope.batchId !== void 0) {
5622
+ const batchId = normalizeTaskBatchId(scope.batchId);
5623
+ scopeConditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
5624
+ }
5539
5625
  return db.transaction((tx) => {
5540
- const runningTasks = tx.select({ count: sql`count(*)` }).from(tasks2).where(eq(tasks2.status, "running")).get();
5626
+ const runningTasks = tx.select({ count: sql`count(*)` }).from(tasks2).where(and(eq(tasks2.status, "running"), ...scopeConditions)).get();
5541
5627
  const runsWithoutRunningTask = tx.select({ count: sql`count(DISTINCT ${taskRuns2.taskId})` }).from(taskRuns2).innerJoin(tasks2, eq(tasks2.id, taskRuns2.taskId)).where(and(
5542
5628
  eq(taskRuns2.status, "running"),
5543
- sql`${tasks2.status} <> 'running'`
5629
+ sql`${tasks2.status} <> 'running'`,
5630
+ ...scopeConditions
5544
5631
  )).get();
5545
5632
  return Number(runningTasks?.count ?? 0) + Number(runsWithoutRunningTask?.count ?? 0);
5546
5633
  }, { behavior: "deferred" });
@@ -5873,15 +5960,17 @@ var TaskService = class {
5873
5960
  }).where(and(...conditions, hasViableDependency())).returning().get() ?? null, { behavior: "immediate" });
5874
5961
  }
5875
5962
  static async retryBatch(batchId, scope = {}) {
5963
+ const normalizedBatchId = normalizeTaskBatchId(batchId);
5964
+ if (!normalizedBatchId) return 0;
5876
5965
  const sqlite2 = getSqlite();
5877
5966
  const scopeFilter = scope.cwd === void 0 ? "" : "AND candidate.cwd = ?";
5878
- const parameters = scope.cwd === void 0 ? [batchId] : [batchId, scope.cwd];
5967
+ const parameters = scope.cwd === void 0 ? [TASK_BATCH_TRIM_CHARACTERS, normalizedBatchId] : [TASK_BATCH_TRIM_CHARACTERS, normalizedBatchId, scope.cwd];
5879
5968
  return db.transaction(() => sqlite2.query(`
5880
5969
  WITH RECURSIVE
5881
5970
  candidate(id, cwd, depends_on) AS MATERIALIZED (
5882
5971
  SELECT candidate.id, candidate.cwd, candidate.depends_on
5883
5972
  FROM tasks AS candidate
5884
- WHERE candidate.batch_id = ?
5973
+ WHERE trim(candidate.batch_id, ?) = ?
5885
5974
  AND candidate.status IN ('failed', 'dead_letter')
5886
5975
  ${scopeFilter}
5887
5976
  ),
@@ -5935,16 +6024,28 @@ var TaskService = class {
5935
6024
  static async list(options = {}) {
5936
6025
  let query = db.select().from(tasks2).$dynamic();
5937
6026
  const conditions = [];
5938
- if (options.status) {
6027
+ if (options.activeExecution) {
6028
+ conditions.push(or(
6029
+ eq(tasks2.status, "running"),
6030
+ sql`EXISTS (
6031
+ SELECT 1 FROM task_runs AS active_list_run
6032
+ WHERE active_list_run.task_id = ${tasks2.id}
6033
+ AND active_list_run.status = 'running'
6034
+ )`
6035
+ ));
6036
+ } else if (options.status) {
5939
6037
  conditions.push(eq(tasks2.status, options.status));
5940
6038
  }
5941
- if (options.batchId) {
5942
- conditions.push(eq(tasks2.batchId, options.batchId));
6039
+ if (options.batchId !== void 0) {
6040
+ const batchId = normalizeTaskBatchId(options.batchId);
6041
+ conditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
5943
6042
  }
5944
6043
  if (options.category) {
5945
6044
  conditions.push(eq(tasks2.category, options.category));
5946
6045
  }
5947
- if (options.cwd !== void 0) {
6046
+ if (options.legacyCwd) {
6047
+ conditions.push(sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`);
6048
+ } else if (options.cwd !== void 0) {
5948
6049
  conditions.push(eq(tasks2.cwd, options.cwd));
5949
6050
  }
5950
6051
  if (conditions.length > 0) {
@@ -5962,9 +6063,12 @@ var TaskService = class {
5962
6063
  static async stats(options = {}) {
5963
6064
  const conditions = [];
5964
6065
  if (options.batchId !== void 0) {
5965
- conditions.push(eq(tasks2.batchId, options.batchId));
6066
+ const batchId = normalizeTaskBatchId(options.batchId);
6067
+ conditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
5966
6068
  }
5967
- if (options.cwd !== void 0) {
6069
+ if (options.legacyCwd) {
6070
+ conditions.push(sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`);
6071
+ } else if (options.cwd !== void 0) {
5968
6072
  conditions.push(eq(tasks2.cwd, options.cwd));
5969
6073
  }
5970
6074
  const whereCondition = conditions.length > 0 ? and(...conditions) : void 0;
@@ -5989,6 +6093,33 @@ var TaskService = class {
5989
6093
  }
5990
6094
  return stats;
5991
6095
  }
6096
+ static async projectSummaries(limit = 100) {
6097
+ this.validateInteger("limit", limit, 1, 1e3);
6098
+ const lastCreatedAt = sql`max(${tasks2.createdAt})`;
6099
+ const lastTaskId = sql`max(${tasks2.id})`;
6100
+ const rows = await db.select({
6101
+ cwd: tasks2.cwd,
6102
+ total: sql`count(*)`,
6103
+ pending: sql`sum(CASE WHEN ${tasks2.status} = 'pending' THEN 1 ELSE 0 END)`,
6104
+ running: sql`sum(CASE WHEN ${tasks2.status} = 'running' OR EXISTS (
6105
+ SELECT 1 FROM task_runs AS active_project_run
6106
+ WHERE active_project_run.task_id = ${tasks2.id}
6107
+ AND active_project_run.status = 'running'
6108
+ ) THEN 1 ELSE 0 END)`,
6109
+ failed: sql`sum(CASE WHEN ${tasks2.status} IN ('failed', 'dead_letter') THEN 1 ELSE 0 END)`,
6110
+ done: sql`sum(CASE WHEN ${tasks2.status} = 'done' THEN 1 ELSE 0 END)`,
6111
+ lastCreatedAt
6112
+ }).from(tasks2).where(sql`${tasks2.cwd} IS NOT NULL AND trim(${tasks2.cwd}) <> ''`).groupBy(tasks2.cwd).orderBy(desc(lastCreatedAt), desc(lastTaskId)).limit(limit);
6113
+ return rows.flatMap((row) => row.cwd === null ? [] : [{
6114
+ cwd: row.cwd,
6115
+ total: Number(row.total),
6116
+ pending: Number(row.pending),
6117
+ running: Number(row.running),
6118
+ failed: Number(row.failed),
6119
+ done: Number(row.done),
6120
+ lastCreatedAt: row.lastCreatedAt === null ? null : Number(row.lastCreatedAt) * 1e3
6121
+ }]);
6122
+ }
5992
6123
  static async delete(id, scope = {}) {
5993
6124
  const conditions = [
5994
6125
  eq(tasks2.id, id),
@@ -6125,11 +6256,15 @@ import { basename, dirname as dirname2, resolve } from "path";
6125
6256
  var TOKEN_GUARDIAN_LAUNCH_PROTOCOL = "gated-v3-token-guardian";
6126
6257
  var LAUNCH_IDENTITY_ARGUMENT = "--supertask-launch-identity";
6127
6258
  var DRAIN_PROOF_MESSAGE_TYPE = "supertask-drained";
6259
+ var DRAIN_PROOF_ACK_MESSAGE_TYPE = "supertask-drained-ack";
6128
6260
  var MANAGED_RUN_ENV = "SUPERTASK_MANAGED_RUN";
6129
6261
  var MANAGED_RUN_ENV_VALUE = "1";
6130
6262
  function isLaunchIdentity(value) {
6131
6263
  return value != null && /^gateway-[1-9]\d*:launch:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
6132
6264
  }
6265
+ function drainProofAckForIdentity(launchIdentity) {
6266
+ return { type: DRAIN_PROOF_ACK_MESSAGE_TYPE, identity: launchIdentity };
6267
+ }
6133
6268
  function isMatchingDrainProof(message, launchIdentity) {
6134
6269
  if (typeof message !== "object" || message == null) return false;
6135
6270
  const candidate = message;
@@ -6627,7 +6762,8 @@ var WorkerEngine = class {
6627
6762
  if (!task) break;
6628
6763
  if (this.stopped) break;
6629
6764
  if (!await TaskService.start(task.id)) continue;
6630
- if (task.batchId) this.activeBatchIds.add(task.batchId);
6765
+ const batchId = normalizeTaskBatchId(task.batchId);
6766
+ if (batchId) this.activeBatchIds.add(batchId);
6631
6767
  if (this.stopped) {
6632
6768
  await TaskService.resetRunningToPending([task.id]);
6633
6769
  this.releaseBatch(task);
@@ -6658,6 +6794,14 @@ var WorkerEngine = class {
6658
6794
  this.releaseBatch(task);
6659
6795
  continue;
6660
6796
  }
6797
+ try {
6798
+ validateTaskWorkingDirectory(task.cwd);
6799
+ } catch (error) {
6800
+ const message = error instanceof Error ? error.message : String(error);
6801
+ await TaskService.failRun(task.id, run.id, message, { setDeadLetter: true });
6802
+ this.releaseBatch(task);
6803
+ continue;
6804
+ }
6661
6805
  await this.spawnTask(task, run.id, launchIdentity);
6662
6806
  } catch (err) {
6663
6807
  this.releaseBatch(task);
@@ -6743,6 +6887,11 @@ var WorkerEngine = class {
6743
6887
  child.on("message", (message) => {
6744
6888
  if (isMatchingDrainProof(message, entry.launchIdentity)) {
6745
6889
  entry.guardianDrained = true;
6890
+ try {
6891
+ child.send(drainProofAckForIdentity(entry.launchIdentity));
6892
+ } catch (error) {
6893
+ this.logError("drain proof acknowledgment failed", error, task.id);
6894
+ }
6746
6895
  }
6747
6896
  });
6748
6897
  let spawnError = null;
@@ -7022,7 +7171,8 @@ ${output}` : ""}` : output;
7022
7171
  );
7023
7172
  }
7024
7173
  releaseBatch(task) {
7025
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
7174
+ const batchId = normalizeTaskBatchId(task.batchId);
7175
+ if (batchId) this.activeBatchIds.delete(batchId);
7026
7176
  }
7027
7177
  resolveModel(taskModel) {
7028
7178
  if (!taskModel || taskModel === "default") return null;