@roamcode.ai/server 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/start.js CHANGED
@@ -4,10 +4,10 @@
4
4
  import { pathToFileURL, fileURLToPath as fileURLToPath3 } from "url";
5
5
  import { join as join19 } from "path";
6
6
  import { existsSync as existsSync7 } from "fs";
7
- import { randomUUID as randomUUID11 } from "crypto";
7
+ import { randomUUID as randomUUID12 } from "crypto";
8
8
 
9
9
  // src/transport.ts
10
- import { createHash as createHash7, randomBytes as randomBytes11, randomUUID as randomUUID7, timingSafeEqual as timingSafeEqual4 } from "crypto";
10
+ import { createHash as createHash7, randomBytes as randomBytes11, randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual4 } from "crypto";
11
11
  import { basename as pathBasename, join as join11, resolve as resolvePath } from "path";
12
12
  import { createReadStream } from "fs";
13
13
  import Fastify from "fastify";
@@ -4493,6 +4493,48 @@ function normalizeTrigger(value) {
4493
4493
  }
4494
4494
  return { type: "manual" };
4495
4495
  }
4496
+ function validTimeZone(value) {
4497
+ try {
4498
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format(0);
4499
+ return true;
4500
+ } catch {
4501
+ return false;
4502
+ }
4503
+ }
4504
+ function normalizeConfiguredTriggers(value) {
4505
+ if (!Array.isArray(value) || value.length > 16) throw new Error("invalid automation triggers");
4506
+ const ids = /* @__PURE__ */ new Set();
4507
+ return value.map((candidate) => {
4508
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
4509
+ throw new Error("invalid automation trigger");
4510
+ }
4511
+ const raw = candidate;
4512
+ const id = normalizeId(raw.id, "automation trigger id");
4513
+ if (ids.has(id)) throw new Error("duplicate automation trigger id");
4514
+ ids.add(id);
4515
+ if (typeof raw.enabled !== "boolean") throw new Error("invalid automation trigger state");
4516
+ if (raw.type === "schedule") {
4517
+ if (typeof raw.cron !== "string" || raw.cron.trim().split(/\s+/).length !== 5 || raw.cron.length > 120 || typeof raw.timeZone !== "string" || raw.timeZone.length > 80 || !validTimeZone(raw.timeZone) || raw.missedRunPolicy !== "skip" || Object.keys(raw).some((key) => !["id", "type", "enabled", "cron", "timeZone", "missedRunPolicy"].includes(key))) {
4518
+ throw new Error("invalid schedule trigger");
4519
+ }
4520
+ return {
4521
+ id,
4522
+ type: "schedule",
4523
+ enabled: raw.enabled,
4524
+ cron: raw.cron.trim().replace(/\s+/g, " "),
4525
+ timeZone: raw.timeZone,
4526
+ missedRunPolicy: "skip"
4527
+ };
4528
+ }
4529
+ if (raw.type === "webhook") {
4530
+ if (typeof raw.hookId !== "string" || !/^rcwh_[A-Za-z0-9_-]{24,80}$/.test(raw.hookId) || typeof raw.secretHash !== "string" || !/^[a-f0-9]{64}$/.test(raw.secretHash) || Object.keys(raw).some((key) => !["id", "type", "enabled", "hookId", "secretHash"].includes(key))) {
4531
+ throw new Error("invalid webhook trigger");
4532
+ }
4533
+ return { id, type: "webhook", enabled: raw.enabled, hookId: raw.hookId, secretHash: raw.secretHash };
4534
+ }
4535
+ throw new Error("invalid automation trigger");
4536
+ });
4537
+ }
4496
4538
  function normalizeRuntimeOptions(value) {
4497
4539
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid runtime options");
4498
4540
  let encoded;
@@ -4519,7 +4561,8 @@ function normalizeCreate(input) {
4519
4561
  cwd: normalizeCwd2(input.cwd),
4520
4562
  instruction: normalizeInstruction(input.instruction),
4521
4563
  runtimeOptions: normalizeRuntimeOptions(input.runtimeOptions ?? {}),
4522
- trigger: normalizeTrigger(input.trigger ?? { type: "manual" })
4564
+ trigger: normalizeTrigger(input.trigger ?? { type: "manual" }),
4565
+ triggers: normalizeConfiguredTriggers(input.triggers ?? [])
4523
4566
  };
4524
4567
  }
4525
4568
  function applyUpdate(current, input, now) {
@@ -4533,7 +4576,8 @@ function applyUpdate(current, input, now) {
4533
4576
  cwd: input.cwd ?? current.cwd,
4534
4577
  instruction: input.instruction ?? current.instruction,
4535
4578
  runtimeOptions: input.runtimeOptions ?? current.runtimeOptions,
4536
- trigger: input.trigger ?? current.trigger
4579
+ trigger: input.trigger ?? current.trigger,
4580
+ triggers: input.triggers ?? current.triggers
4537
4581
  };
4538
4582
  if (Object.values(input).some((value) => value === void 0) || typeof candidate.enabled !== "boolean") {
4539
4583
  throw new Error("invalid automation update");
@@ -4555,9 +4599,12 @@ function createMemoryStore3(opts) {
4555
4599
  const removedDefinitions = /* @__PURE__ */ new Set();
4556
4600
  const runs = /* @__PURE__ */ new Map();
4557
4601
  const runInputSnapshots = /* @__PURE__ */ new Map();
4602
+ const activities = /* @__PURE__ */ new Map();
4603
+ const triggerCursors = /* @__PURE__ */ new Map();
4558
4604
  const nodeOwners = /* @__PURE__ */ new Map();
4559
4605
  const generateAutomationId = opts.generateAutomationId ?? (() => randomId3("rca2"));
4560
4606
  const generateRunId = opts.generateRunId ?? (() => randomId3("rcar"));
4607
+ const generateActivityId = opts.generateActivityId ?? (() => randomId3("rcae"));
4561
4608
  return {
4562
4609
  mode: "memory-fallback",
4563
4610
  getNodeOwner(nodeId) {
@@ -4643,8 +4690,8 @@ function createMemoryStore3(opts) {
4643
4690
  const snapshot = runInputSnapshots.get(id);
4644
4691
  return snapshot ? clone2(snapshot) : void 0;
4645
4692
  },
4646
- getRunByInvocationId(invocationId) {
4647
- const run = [...runs.values()].find((candidate) => candidate.invocationId === invocationId);
4693
+ getRunByInvocationId(invocationId2) {
4694
+ const run = [...runs.values()].find((candidate) => candidate.invocationId === invocationId2);
4648
4695
  return run ? clone2(run) : void 0;
4649
4696
  },
4650
4697
  getRunBySessionId(sessionId) {
@@ -4742,11 +4789,43 @@ function createMemoryStore3(opts) {
4742
4789
  const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
4743
4790
  return [...runs.values()].filter((run) => !automationId || run.automationId === automationId).sort((a, b) => b.createdAt - a.createdAt || b.id.localeCompare(a.id)).slice(0, bounded).map(clone2);
4744
4791
  },
4792
+ getTriggerCursor(triggerId) {
4793
+ return triggerCursors.get(normalizeId(triggerId, "automation trigger id"));
4794
+ },
4795
+ setTriggerCursor(triggerId, minute) {
4796
+ if (!Number.isSafeInteger(minute) || minute < 0) throw new Error("invalid automation trigger cursor");
4797
+ triggerCursors.set(normalizeId(triggerId, "automation trigger id"), minute);
4798
+ },
4799
+ createActivity(input, now = Date.now()) {
4800
+ const id = normalizeId(generateActivityId(), "automation activity id");
4801
+ if (activities.has(id)) throw new Error("automation activity id already exists");
4802
+ const activity = { id, ...clone2(input), createdAt: now, updatedAt: now };
4803
+ activities.set(id, activity);
4804
+ return clone2(activity);
4805
+ },
4806
+ getActivityByInvocationId(invocationId2) {
4807
+ const normalized = normalizeId(invocationId2, "invocation id");
4808
+ const activity = [...activities.values()].find((candidate) => candidate.invocationId === normalized);
4809
+ return activity ? clone2(activity) : void 0;
4810
+ },
4811
+ updateActivity(id, input, now = Date.now()) {
4812
+ const current = activities.get(id);
4813
+ if (!current) return void 0;
4814
+ const next = { ...current, ...clone2(input), updatedAt: now };
4815
+ activities.set(id, next);
4816
+ return clone2(next);
4817
+ },
4818
+ listActivities(automationId, limit = 100) {
4819
+ const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
4820
+ return [...activities.values()].filter((activity) => !automationId || activity.automationId === automationId).sort((left, right) => right.createdAt - left.createdAt || right.id.localeCompare(left.id)).slice(0, bounded).map(clone2);
4821
+ },
4745
4822
  close() {
4746
4823
  definitions.clear();
4747
4824
  removedDefinitions.clear();
4748
4825
  runs.clear();
4749
4826
  runInputSnapshots.clear();
4827
+ activities.clear();
4828
+ triggerCursors.clear();
4750
4829
  nodeOwners.clear();
4751
4830
  }
4752
4831
  };
@@ -4763,7 +4842,11 @@ function definitionFromRow(row) {
4763
4842
  cwd: row.cwd,
4764
4843
  instruction: row.instruction,
4765
4844
  runtimeOptions: JSON.parse(row.runtime_options_json),
4766
- trigger: normalizeTrigger(JSON.parse(row.trigger_json)),
4845
+ trigger: { type: "manual" },
4846
+ triggers: (() => {
4847
+ const parsed = JSON.parse(row.trigger_json);
4848
+ return Array.isArray(parsed) ? normalizeConfiguredTriggers(parsed) : [];
4849
+ })(),
4767
4850
  revision: row.revision,
4768
4851
  createdAt: row.created_at,
4769
4852
  updatedAt: row.updated_at
@@ -4796,6 +4879,22 @@ function runInputSnapshotFromRow(row) {
4796
4879
  bootstrapState: row.bootstrap_state
4797
4880
  };
4798
4881
  }
4882
+ function activityFromRow(row) {
4883
+ return {
4884
+ id: row.id,
4885
+ automationId: row.automation_id,
4886
+ triggerId: row.trigger_id,
4887
+ source: row.source,
4888
+ status: row.status,
4889
+ invocationId: row.invocation_id,
4890
+ ...row.scheduled_for === null ? {} : { scheduledFor: row.scheduled_for },
4891
+ ...row.missed_count === null ? {} : { missedCount: row.missed_count },
4892
+ ...row.run_id === null ? {} : { runId: row.run_id },
4893
+ ...row.failure_code === null ? {} : { failureCode: row.failure_code },
4894
+ createdAt: row.created_at,
4895
+ updatedAt: row.updated_at
4896
+ };
4897
+ }
4799
4898
  function openSessionAutomationStore(opts) {
4800
4899
  let Database;
4801
4900
  try {
@@ -4859,6 +4958,27 @@ function openSessionAutomationStore(opts) {
4859
4958
  bootstrap_state TEXT NOT NULL DEFAULT 'pending'
4860
4959
  CHECK(bootstrap_state IN ('pending','submitting','submitted'))
4861
4960
  );
4961
+ CREATE TABLE IF NOT EXISTS session_automation_trigger_cursors (
4962
+ trigger_id TEXT PRIMARY KEY,
4963
+ minute INTEGER NOT NULL CHECK(minute >= 0),
4964
+ updated_at INTEGER NOT NULL
4965
+ );
4966
+ CREATE TABLE IF NOT EXISTS session_automation_activities (
4967
+ id TEXT PRIMARY KEY,
4968
+ automation_id TEXT NOT NULL REFERENCES session_automations(id) ON DELETE CASCADE,
4969
+ trigger_id TEXT NOT NULL,
4970
+ source TEXT NOT NULL CHECK(source IN ('schedule','webhook')),
4971
+ status TEXT NOT NULL CHECK(status IN ('queued','started','failed','missed','expired')),
4972
+ invocation_id TEXT NOT NULL UNIQUE,
4973
+ scheduled_for INTEGER,
4974
+ missed_count INTEGER CHECK(missed_count IS NULL OR missed_count > 0),
4975
+ run_id TEXT REFERENCES session_automation_runs(id) ON DELETE SET NULL,
4976
+ failure_code TEXT,
4977
+ created_at INTEGER NOT NULL,
4978
+ updated_at INTEGER NOT NULL
4979
+ );
4980
+ CREATE INDEX IF NOT EXISTS session_automation_activities_definition_idx
4981
+ ON session_automation_activities(automation_id, created_at DESC);
4862
4982
  `);
4863
4983
  const automationColumns = db.pragma("table_info(session_automations)");
4864
4984
  if (!automationColumns.some((column) => column.name === "deleted_at")) {
@@ -4872,6 +4992,7 @@ function openSessionAutomationStore(opts) {
4872
4992
  }
4873
4993
  const generateAutomationId = opts.generateAutomationId ?? (() => randomId3("rca2"));
4874
4994
  const generateRunId = opts.generateRunId ?? (() => randomId3("rcar"));
4995
+ const generateActivityId = opts.generateActivityId ?? (() => randomId3("rcae"));
4875
4996
  const getDefinition = db.prepare("SELECT * FROM session_automations WHERE id = ? AND deleted_at IS NULL");
4876
4997
  const getDefinitionIncludingRemoved = db.prepare("SELECT * FROM session_automations WHERE id = ?");
4877
4998
  const listAll = db.prepare("SELECT * FROM session_automations WHERE deleted_at IS NULL ORDER BY updated_at DESC, id");
@@ -4898,6 +5019,30 @@ function openSessionAutomationStore(opts) {
4898
5019
  const listRunsForAutomation = db.prepare(
4899
5020
  "SELECT * FROM session_automation_runs WHERE automation_id = ? ORDER BY created_at DESC, id DESC LIMIT ?"
4900
5021
  );
5022
+ const getTriggerCursorStatement = db.prepare(
5023
+ "SELECT minute FROM session_automation_trigger_cursors WHERE trigger_id = ?"
5024
+ );
5025
+ const setTriggerCursorStatement = db.prepare(
5026
+ `INSERT INTO session_automation_trigger_cursors(trigger_id,minute,updated_at) VALUES(?,?,?)
5027
+ ON CONFLICT(trigger_id) DO UPDATE SET minute=excluded.minute,updated_at=excluded.updated_at`
5028
+ );
5029
+ const getActivity = db.prepare("SELECT * FROM session_automation_activities WHERE id = ?");
5030
+ const getActivityByInvocationId = db.prepare("SELECT * FROM session_automation_activities WHERE invocation_id = ?");
5031
+ const insertActivity = db.prepare(
5032
+ `INSERT INTO session_automation_activities(
5033
+ id,automation_id,trigger_id,source,status,invocation_id,scheduled_for,missed_count,run_id,failure_code,
5034
+ created_at,updated_at
5035
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`
5036
+ );
5037
+ const updateActivityStatement = db.prepare(
5038
+ `UPDATE session_automation_activities SET status=?,run_id=?,failure_code=?,updated_at=? WHERE id=?`
5039
+ );
5040
+ const listActivitiesAll = db.prepare(
5041
+ "SELECT * FROM session_automation_activities ORDER BY created_at DESC, id DESC LIMIT ?"
5042
+ );
5043
+ const listActivitiesForAutomation = db.prepare(
5044
+ "SELECT * FROM session_automation_activities WHERE automation_id = ? ORDER BY created_at DESC, id DESC LIMIT ?"
5045
+ );
4901
5046
  return {
4902
5047
  mode: "sqlite",
4903
5048
  getNodeOwner(nodeId) {
@@ -4937,7 +5082,7 @@ function openSessionAutomationStore(opts) {
4937
5082
  definition.cwd,
4938
5083
  definition.instruction,
4939
5084
  JSON.stringify(definition.runtimeOptions),
4940
- JSON.stringify(definition.trigger),
5085
+ JSON.stringify(definition.triggers),
4941
5086
  definition.revision,
4942
5087
  definition.createdAt,
4943
5088
  definition.updatedAt
@@ -4959,7 +5104,7 @@ function openSessionAutomationStore(opts) {
4959
5104
  next.cwd,
4960
5105
  next.instruction,
4961
5106
  JSON.stringify(next.runtimeOptions),
4962
- JSON.stringify(next.trigger),
5107
+ JSON.stringify(next.triggers),
4963
5108
  next.revision,
4964
5109
  next.updatedAt,
4965
5110
  next.id,
@@ -5020,8 +5165,8 @@ function openSessionAutomationStore(opts) {
5020
5165
  const row = selectRunInputSnapshot.get(id);
5021
5166
  return row ? runInputSnapshotFromRow(row) : void 0;
5022
5167
  },
5023
- getRunByInvocationId(invocationId) {
5024
- const row = getRunByInvocationId.get(invocationId);
5168
+ getRunByInvocationId(invocationId2) {
5169
+ const row = getRunByInvocationId.get(invocationId2);
5025
5170
  return row ? runFromRow(row) : void 0;
5026
5171
  },
5027
5172
  getRunBySessionId(sessionId) {
@@ -5132,12 +5277,266 @@ function openSessionAutomationStore(opts) {
5132
5277
  const rows = automationId ? listRunsForAutomation.all(automationId, bounded) : listRunsAll.all(bounded);
5133
5278
  return rows.map(runFromRow);
5134
5279
  },
5280
+ getTriggerCursor(triggerId) {
5281
+ const row = getTriggerCursorStatement.get(normalizeId(triggerId, "automation trigger id"));
5282
+ return row?.minute;
5283
+ },
5284
+ setTriggerCursor(triggerId, minute) {
5285
+ if (!Number.isSafeInteger(minute) || minute < 0) throw new Error("invalid automation trigger cursor");
5286
+ setTriggerCursorStatement.run(normalizeId(triggerId, "automation trigger id"), minute, Date.now());
5287
+ },
5288
+ createActivity(input, now = Date.now()) {
5289
+ const activity = {
5290
+ id: normalizeId(generateActivityId(), "automation activity id"),
5291
+ ...input,
5292
+ automationId: normalizeId(input.automationId, "automation id"),
5293
+ triggerId: normalizeId(input.triggerId, "automation trigger id"),
5294
+ invocationId: normalizeId(input.invocationId, "invocation id"),
5295
+ createdAt: now,
5296
+ updatedAt: now
5297
+ };
5298
+ insertActivity.run(
5299
+ activity.id,
5300
+ activity.automationId,
5301
+ activity.triggerId,
5302
+ activity.source,
5303
+ activity.status,
5304
+ activity.invocationId,
5305
+ activity.scheduledFor ?? null,
5306
+ activity.missedCount ?? null,
5307
+ activity.runId ?? null,
5308
+ activity.failureCode ?? null,
5309
+ activity.createdAt,
5310
+ activity.updatedAt
5311
+ );
5312
+ return activity;
5313
+ },
5314
+ getActivityByInvocationId(invocationId2) {
5315
+ const row = getActivityByInvocationId.get(normalizeId(invocationId2, "invocation id"));
5316
+ return row ? activityFromRow(row) : void 0;
5317
+ },
5318
+ updateActivity(id, input, now = Date.now()) {
5319
+ const row = getActivity.get(id);
5320
+ if (!row) return void 0;
5321
+ const current = activityFromRow(row);
5322
+ const next = { ...current, ...input, updatedAt: now };
5323
+ const result = updateActivityStatement.run(
5324
+ next.status,
5325
+ next.runId ?? null,
5326
+ next.failureCode ?? null,
5327
+ next.updatedAt,
5328
+ current.id
5329
+ );
5330
+ return result.changes === 1 ? next : void 0;
5331
+ },
5332
+ listActivities(automationId, limit = 100) {
5333
+ const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
5334
+ const rows = automationId ? listActivitiesForAutomation.all(automationId, bounded) : listActivitiesAll.all(bounded);
5335
+ return rows.map(activityFromRow);
5336
+ },
5135
5337
  close() {
5136
5338
  db.close();
5137
5339
  }
5138
5340
  };
5139
5341
  }
5140
5342
 
5343
+ // src/automation-trigger-engine.ts
5344
+ import { randomUUID as randomUUID3 } from "crypto";
5345
+ function parsePart(part, min, max, normalize) {
5346
+ const [rangePart, stepPart] = part.split("/");
5347
+ const step = stepPart === void 0 ? 1 : Number(stepPart);
5348
+ if (!Number.isSafeInteger(step) || step < 1 || !rangePart) throw new Error("invalid cron field");
5349
+ let start;
5350
+ let end;
5351
+ if (rangePart === "*") {
5352
+ start = min;
5353
+ end = max;
5354
+ } else if (rangePart.includes("-")) {
5355
+ const pair = rangePart.split("-");
5356
+ if (pair.length !== 2) throw new Error("invalid cron field");
5357
+ start = Number(pair[0]);
5358
+ end = Number(pair[1]);
5359
+ } else {
5360
+ start = Number(rangePart);
5361
+ end = start;
5362
+ }
5363
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < min || end > max || start > end) {
5364
+ throw new Error("invalid cron field");
5365
+ }
5366
+ const values = [];
5367
+ for (let value = start; value <= end; value += step) values.push(normalize ? normalize(value) : value);
5368
+ return values;
5369
+ }
5370
+ function parseField(source, min, max, normalize) {
5371
+ const values = /* @__PURE__ */ new Set();
5372
+ for (const part of source.split(",")) {
5373
+ for (const value of parsePart(part, min, max, normalize)) values.add(value);
5374
+ }
5375
+ return { any: source === "*", values };
5376
+ }
5377
+ function validateCronExpression(expression) {
5378
+ const normalized = expression.trim().replace(/\s+/g, " ");
5379
+ const fields = normalized.split(" ");
5380
+ if (fields.length !== 5) throw new Error("cron must contain five fields");
5381
+ parseField(fields[0], 0, 59);
5382
+ parseField(fields[1], 0, 23);
5383
+ parseField(fields[2], 1, 31);
5384
+ parseField(fields[3], 1, 12);
5385
+ parseField(fields[4], 0, 7, (value) => value === 7 ? 0 : value);
5386
+ return normalized;
5387
+ }
5388
+ function zonedParts(time, timeZone) {
5389
+ const parts = new Intl.DateTimeFormat("en-US", {
5390
+ timeZone,
5391
+ minute: "2-digit",
5392
+ hour: "2-digit",
5393
+ hourCycle: "h23",
5394
+ day: "2-digit",
5395
+ month: "2-digit",
5396
+ weekday: "short"
5397
+ }).formatToParts(time);
5398
+ const byType = new Map(parts.map((part) => [part.type, part.value]));
5399
+ const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(byType.get("weekday") ?? "");
5400
+ return {
5401
+ minute: Number(byType.get("minute")),
5402
+ hour: Number(byType.get("hour")),
5403
+ day: Number(byType.get("day")),
5404
+ month: Number(byType.get("month")),
5405
+ weekday
5406
+ };
5407
+ }
5408
+ function cronMatches(expression, timeZone, time) {
5409
+ const fields = validateCronExpression(expression).split(" ");
5410
+ const minute = parseField(fields[0], 0, 59);
5411
+ const hour = parseField(fields[1], 0, 23);
5412
+ const day = parseField(fields[2], 1, 31);
5413
+ const month = parseField(fields[3], 1, 12);
5414
+ const weekday = parseField(fields[4], 0, 7, (value2) => value2 === 7 ? 0 : value2);
5415
+ const value = zonedParts(time, timeZone);
5416
+ const dayMatches = day.values.has(value.day);
5417
+ const weekdayMatches = weekday.values.has(value.weekday);
5418
+ const calendarMatches = day.any && weekday.any ? true : day.any ? weekdayMatches : weekday.any ? dayMatches : dayMatches || weekdayMatches;
5419
+ return minute.values.has(value.minute) && hour.values.has(value.hour) && month.values.has(value.month) && calendarMatches;
5420
+ }
5421
+ function invocationId(source, automationId, triggerId, nonce) {
5422
+ const safeNonce = nonce.replace(/[^A-Za-z0-9._:-]/g, "_").slice(0, 96);
5423
+ return `rci_${source}_${automationId}_${triggerId}_${safeNonce}`.slice(0, 256);
5424
+ }
5425
+ function createAutomationTriggerEngine(options) {
5426
+ const now = options.now ?? Date.now;
5427
+ const interval = options.setInterval ?? globalThis.setInterval;
5428
+ const clearInterval2 = options.clearInterval ?? globalThis.clearInterval;
5429
+ const concurrency = Number.isSafeInteger(options.concurrency) && (options.concurrency ?? 0) > 0 ? options.concurrency : 2;
5430
+ const pending = [];
5431
+ const pendingIds = /* @__PURE__ */ new Set();
5432
+ const inFlightIds = /* @__PURE__ */ new Set();
5433
+ let active = 0;
5434
+ let ticking = false;
5435
+ let timer;
5436
+ const queue = (activity, deferDrain = false) => {
5437
+ if (activity.status !== "queued" || pendingIds.has(activity.id) || inFlightIds.has(activity.id)) return;
5438
+ pendingIds.add(activity.id);
5439
+ pending.push(activity);
5440
+ pending.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
5441
+ if (!deferDrain) drain();
5442
+ };
5443
+ const drain = () => {
5444
+ while (active < concurrency && pending.length > 0) {
5445
+ const activity = pending.shift();
5446
+ pendingIds.delete(activity.id);
5447
+ inFlightIds.add(activity.id);
5448
+ active += 1;
5449
+ void options.execute(activity).then(({ runId }) => options.store.updateActivity(activity.id, { status: "started", runId })).catch(
5450
+ () => options.store.updateActivity(activity.id, { status: "failed", failureCode: "TRIGGER_EXECUTION_FAILED" })
5451
+ ).finally(() => {
5452
+ inFlightIds.delete(activity.id);
5453
+ active -= 1;
5454
+ drain();
5455
+ });
5456
+ }
5457
+ };
5458
+ const createQueued = (automation, trigger, source, nonce, scheduledFor) => {
5459
+ const id = invocationId(source, automation.id, trigger.id, nonce);
5460
+ const existing = options.store.getActivityByInvocationId(id);
5461
+ if (existing) {
5462
+ queue(existing);
5463
+ return existing;
5464
+ }
5465
+ const activity = options.store.createActivity({
5466
+ automationId: automation.id,
5467
+ triggerId: trigger.id,
5468
+ source,
5469
+ status: "queued",
5470
+ invocationId: id,
5471
+ ...scheduledFor === void 0 ? {} : { scheduledFor }
5472
+ });
5473
+ queue(activity);
5474
+ return activity;
5475
+ };
5476
+ const tick = async (at = now()) => {
5477
+ if (ticking) return;
5478
+ ticking = true;
5479
+ try {
5480
+ const currentMinute = Math.floor(at / 6e4);
5481
+ for (const automation of options.store.list()) {
5482
+ if (!automation.enabled) continue;
5483
+ for (const trigger of automation.triggers) {
5484
+ if (trigger.type !== "schedule" || !trigger.enabled) continue;
5485
+ const previous = options.store.getTriggerCursor(trigger.id) ?? currentMinute - 1;
5486
+ if (previous >= currentMinute) continue;
5487
+ const firstMinute = Math.max(previous + 1, currentMinute - 43200);
5488
+ const matches = [];
5489
+ for (let minute = firstMinute; minute <= currentMinute; minute += 1) {
5490
+ if (cronMatches(trigger.cron, trigger.timeZone, minute * 6e4)) matches.push(minute);
5491
+ }
5492
+ const missed = matches.filter((minute) => minute < currentMinute);
5493
+ if (missed.length > 0) {
5494
+ const missedInvocation = invocationId("schedule", automation.id, trigger.id, `missed-${currentMinute}`);
5495
+ const exists = options.store.listActivities(automation.id, 1e3).some((activity) => activity.invocationId === missedInvocation);
5496
+ if (!exists) {
5497
+ options.store.createActivity({
5498
+ automationId: automation.id,
5499
+ triggerId: trigger.id,
5500
+ source: "schedule",
5501
+ status: "missed",
5502
+ invocationId: missedInvocation,
5503
+ scheduledFor: missed[missed.length - 1] * 6e4,
5504
+ missedCount: missed.length,
5505
+ failureCode: "NODE_OFFLINE_MISSED_SCHEDULE"
5506
+ });
5507
+ }
5508
+ }
5509
+ if (matches.includes(currentMinute)) {
5510
+ createQueued(automation, trigger, "schedule", String(currentMinute), currentMinute * 6e4);
5511
+ }
5512
+ options.store.setTriggerCursor(trigger.id, currentMinute);
5513
+ }
5514
+ }
5515
+ } finally {
5516
+ ticking = false;
5517
+ }
5518
+ };
5519
+ return {
5520
+ start() {
5521
+ if (timer) return;
5522
+ for (const activity of options.store.listActivities(void 0, 1e3)) queue(activity, true);
5523
+ drain();
5524
+ void tick();
5525
+ timer = interval(() => void tick(), 15e3);
5526
+ },
5527
+ stop() {
5528
+ if (!timer) return;
5529
+ clearInterval2(timer);
5530
+ timer = void 0;
5531
+ },
5532
+ tick,
5533
+ enqueueWebhook(automation, trigger, externalInvocationId) {
5534
+ if (trigger.type !== "webhook") throw new Error("invalid webhook trigger");
5535
+ return createQueued(automation, trigger, "webhook", externalInvocationId ?? randomUUID3());
5536
+ }
5537
+ };
5538
+ }
5539
+
5141
5540
  // src/node-domain.ts
5142
5541
  import { createHash as createHash5 } from "crypto";
5143
5542
  function productContextFromOwner(owner, name) {
@@ -5211,7 +5610,7 @@ function projectAgentRuntimeRecords(input) {
5211
5610
 
5212
5611
  // src/updater.ts
5213
5612
  import { spawn as nodeSpawn } from "child_process";
5214
- import { randomUUID as randomUUID4 } from "crypto";
5613
+ import { randomUUID as randomUUID5 } from "crypto";
5215
5614
  import {
5216
5615
  chmodSync as nodeChmodSync,
5217
5616
  existsSync as nodeExistsSync,
@@ -5225,7 +5624,7 @@ import { fileURLToPath } from "url";
5225
5624
 
5226
5625
  // src/managed-runtime.ts
5227
5626
  import { spawn } from "child_process";
5228
- import { randomUUID as randomUUID3 } from "crypto";
5627
+ import { randomUUID as randomUUID4 } from "crypto";
5229
5628
  import {
5230
5629
  chmodSync as chmodSync2,
5231
5630
  existsSync as existsSync4,
@@ -5323,7 +5722,7 @@ function readPreviousVersion(root) {
5323
5722
  }
5324
5723
 
5325
5724
  // src/updater.ts
5326
- var RUNNING_VERSION = "1.3.0" ? "1.3.0".replace(/^v/, "") : packageVersion();
5725
+ var RUNNING_VERSION = "1.4.0" ? "1.4.0".replace(/^v/, "") : packageVersion();
5327
5726
  var RELEASES_API = "https://api.github.com/repos/burakgon/roamcode/releases?per_page=100";
5328
5727
  var RELEASE_MANIFEST_ASSET = "roamcode-release.json";
5329
5728
  var CHECK_CACHE_MS = 15 * 6e4;
@@ -5622,7 +6021,7 @@ var Updater = class {
5622
6021
  const path = join6(this.dataDir, "update-status.json");
5623
6022
  const value = JSON.stringify(status, null, 2) + "\n";
5624
6023
  if (this.fs.renameSync) {
5625
- const temp = `${path}.${process.pid}.${randomUUID4()}.tmp`;
6024
+ const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
5626
6025
  this.fs.writeFileSync(temp, value, 384);
5627
6026
  this.fs.renameSync(temp, path);
5628
6027
  } else {
@@ -5685,7 +6084,7 @@ var Updater = class {
5685
6084
  return { started: false, reason: error instanceof Error ? error.message : String(error) };
5686
6085
  }
5687
6086
  }
5688
- const operationId = randomUUID4();
6087
+ const operationId = randomUUID5();
5689
6088
  const status = {
5690
6089
  operationId,
5691
6090
  state: "starting",
@@ -10702,9 +11101,12 @@ function buildOpenApiDocument(options) {
10702
11101
  responses: {
10703
11102
  "201": response("Created session automation", {
10704
11103
  type: "object",
10705
- required: ["automation"],
11104
+ required: ["automation", "webhookSecrets"],
10706
11105
  additionalProperties: false,
10707
- properties: { automation: ref("SessionAutomationDefinition") }
11106
+ properties: {
11107
+ automation: ref("SessionAutomationDefinition"),
11108
+ webhookSecrets: { type: "array", items: ref("SessionAutomationWebhookSecret") }
11109
+ }
10708
11110
  }),
10709
11111
  "404": response("Node or runtime not found", ref("Error")),
10710
11112
  ...errorResponses
@@ -10733,9 +11135,12 @@ function buildOpenApiDocument(options) {
10733
11135
  responses: {
10734
11136
  "200": response("Updated session automation", {
10735
11137
  type: "object",
10736
- required: ["automation"],
11138
+ required: ["automation", "webhookSecrets"],
10737
11139
  additionalProperties: false,
10738
- properties: { automation: ref("SessionAutomationDefinition") }
11140
+ properties: {
11141
+ automation: ref("SessionAutomationDefinition"),
11142
+ webhookSecrets: { type: "array", items: ref("SessionAutomationWebhookSecret") }
11143
+ }
10739
11144
  }),
10740
11145
  "404": response("Automation, node, or runtime not found", ref("Error")),
10741
11146
  ...errorResponses
@@ -10751,6 +11156,76 @@ function buildOpenApiDocument(options) {
10751
11156
  }
10752
11157
  }
10753
11158
  },
11159
+ "/api/v2/automations/{automationId}/activity": {
11160
+ get: {
11161
+ operationId: "listSessionAutomationActivityV2",
11162
+ description: "Returns durable schedule and webhook delivery activity, including explicit missed schedules.",
11163
+ parameters: [
11164
+ idParameter("automationId"),
11165
+ { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 100, default: 25 } }
11166
+ ],
11167
+ responses: {
11168
+ "200": response("Bounded automation trigger activity", {
11169
+ type: "object",
11170
+ required: ["activities"],
11171
+ additionalProperties: false,
11172
+ properties: { activities: { type: "array", items: ref("SessionAutomationActivity") } }
11173
+ }),
11174
+ "404": response("Automation not found", ref("Error")),
11175
+ ...errorResponses
11176
+ }
11177
+ }
11178
+ },
11179
+ "/api/v2/automations/{automationId}/triggers/{triggerId}/secret": {
11180
+ post: {
11181
+ operationId: "rotateSessionAutomationWebhookSecretV2",
11182
+ parameters: [idParameter("automationId"), idParameter("triggerId"), idempotency],
11183
+ requestBody: {
11184
+ required: true,
11185
+ content: json({
11186
+ type: "object",
11187
+ required: ["expectedRevision"],
11188
+ additionalProperties: false,
11189
+ properties: { expectedRevision: { type: "integer", minimum: 1 } }
11190
+ })
11191
+ },
11192
+ responses: {
11193
+ "200": response("Rotated webhook secret shown exactly once", {
11194
+ type: "object",
11195
+ required: ["automation", "webhookSecret"],
11196
+ additionalProperties: false,
11197
+ properties: {
11198
+ automation: ref("SessionAutomationDefinition"),
11199
+ webhookSecret: ref("SessionAutomationWebhookSecret")
11200
+ }
11201
+ }),
11202
+ "404": response("Automation or webhook trigger not found", ref("Error")),
11203
+ ...errorResponses
11204
+ }
11205
+ }
11206
+ },
11207
+ "/api/v2/automation-hooks/{hookId}": {
11208
+ post: {
11209
+ operationId: "signalSessionAutomationWebhookV2",
11210
+ description: "Queues a signal-only webhook. The request body is discarded and never becomes task instruction or stored prompt content.",
11211
+ security: [{ webhookAuth: [] }],
11212
+ parameters: [idParameter("hookId")],
11213
+ requestBody: {
11214
+ required: false,
11215
+ content: json({ description: "Ignored signal payload", nullable: true })
11216
+ },
11217
+ responses: {
11218
+ "202": response("Webhook signal accepted", {
11219
+ type: "object",
11220
+ required: ["accepted"],
11221
+ additionalProperties: false,
11222
+ properties: { accepted: { const: true } }
11223
+ }),
11224
+ "401": response("Unknown hook or invalid webhook secret", ref("Error")),
11225
+ "429": response("Webhook rate limit reached", ref("Error"))
11226
+ }
11227
+ }
11228
+ },
10754
11229
  "/api/v2/automations/{automationId}/runs": {
10755
11230
  get: {
10756
11231
  operationId: "listSessionAutomationRunsV2",
@@ -10808,7 +11283,8 @@ function buildOpenApiDocument(options) {
10808
11283
  },
10809
11284
  components: {
10810
11285
  securitySchemes: {
10811
- bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "RoamCode device credential" }
11286
+ bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "RoamCode device credential" },
11287
+ webhookAuth: { type: "http", scheme: "bearer", bearerFormat: "One-time-shown webhook secret" }
10812
11288
  },
10813
11289
  schemas: {
10814
11290
  Error: {
@@ -11782,6 +12258,62 @@ function buildOpenApiDocument(options) {
11782
12258
  additionalProperties: false,
11783
12259
  properties: { type: { const: "manual" } }
11784
12260
  },
12261
+ SessionAutomationConfiguredTrigger: {
12262
+ oneOf: [
12263
+ {
12264
+ type: "object",
12265
+ required: ["id", "type", "enabled", "cron", "timeZone", "missedRunPolicy"],
12266
+ additionalProperties: false,
12267
+ properties: {
12268
+ id: { type: "string", minLength: 1, maxLength: 256 },
12269
+ type: { const: "schedule" },
12270
+ enabled: { type: "boolean" },
12271
+ cron: { type: "string", minLength: 9, maxLength: 120 },
12272
+ timeZone: { type: "string", minLength: 1, maxLength: 80 },
12273
+ missedRunPolicy: { const: "skip" }
12274
+ }
12275
+ },
12276
+ {
12277
+ type: "object",
12278
+ required: ["id", "type", "enabled", "hookId"],
12279
+ additionalProperties: false,
12280
+ properties: {
12281
+ id: { type: "string", minLength: 1, maxLength: 256 },
12282
+ type: { const: "webhook" },
12283
+ enabled: { type: "boolean" },
12284
+ hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" }
12285
+ }
12286
+ }
12287
+ ]
12288
+ },
12289
+ SessionAutomationTriggerInput: {
12290
+ oneOf: [
12291
+ {
12292
+ type: "object",
12293
+ required: ["type", "enabled", "cron", "timeZone", "missedRunPolicy"],
12294
+ additionalProperties: false,
12295
+ properties: {
12296
+ id: { type: "string", minLength: 1, maxLength: 256 },
12297
+ type: { const: "schedule" },
12298
+ enabled: { type: "boolean" },
12299
+ cron: { type: "string", minLength: 9, maxLength: 120 },
12300
+ timeZone: { type: "string", minLength: 1, maxLength: 80 },
12301
+ missedRunPolicy: { const: "skip" }
12302
+ }
12303
+ },
12304
+ {
12305
+ type: "object",
12306
+ required: ["type", "enabled"],
12307
+ additionalProperties: false,
12308
+ properties: {
12309
+ id: { type: "string", minLength: 1, maxLength: 256 },
12310
+ type: { const: "webhook" },
12311
+ enabled: { type: "boolean" },
12312
+ hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" }
12313
+ }
12314
+ }
12315
+ ]
12316
+ },
11785
12317
  SessionAutomationDefinition: {
11786
12318
  type: "object",
11787
12319
  required: [
@@ -11794,6 +12326,7 @@ function buildOpenApiDocument(options) {
11794
12326
  "provider",
11795
12327
  "cwd",
11796
12328
  "trigger",
12329
+ "triggers",
11797
12330
  "instruction",
11798
12331
  "runtimeOptions",
11799
12332
  "revision",
@@ -11811,6 +12344,7 @@ function buildOpenApiDocument(options) {
11811
12344
  provider: { type: "string", pattern: "^[a-z][a-z0-9-]{0,63}$" },
11812
12345
  cwd: { type: "string", minLength: 1 },
11813
12346
  trigger: ref("SessionAutomationTrigger"),
12347
+ triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationConfiguredTrigger") },
11814
12348
  instruction: {
11815
12349
  type: "string",
11816
12350
  minLength: 1,
@@ -11835,6 +12369,7 @@ function buildOpenApiDocument(options) {
11835
12369
  agentRuntimeId: { type: "string", pattern: "^runtime_[A-Za-z0-9_-]{24}$" },
11836
12370
  cwd: { type: "string", minLength: 1 },
11837
12371
  trigger: ref("SessionAutomationTrigger"),
12372
+ triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationTriggerInput") },
11838
12373
  instruction: {
11839
12374
  type: "string",
11840
12375
  minLength: 1,
@@ -11858,6 +12393,7 @@ function buildOpenApiDocument(options) {
11858
12393
  agentRuntimeId: { type: "string", pattern: "^runtime_[A-Za-z0-9_-]{24}$" },
11859
12394
  cwd: { type: "string", minLength: 1 },
11860
12395
  trigger: ref("SessionAutomationTrigger"),
12396
+ triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationTriggerInput") },
11861
12397
  instruction: {
11862
12398
  type: "string",
11863
12399
  minLength: 1,
@@ -11868,6 +12404,36 @@ function buildOpenApiDocument(options) {
11868
12404
  runtimeOptions: { type: "object", additionalProperties: true, "x-maxBytes": 65536 }
11869
12405
  }
11870
12406
  },
12407
+ SessionAutomationWebhookSecret: {
12408
+ type: "object",
12409
+ required: ["triggerId", "hookId", "secret", "path"],
12410
+ additionalProperties: false,
12411
+ properties: {
12412
+ triggerId: { type: "string", minLength: 1, maxLength: 256 },
12413
+ hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" },
12414
+ secret: { type: "string", pattern: "^rcws_[A-Za-z0-9_-]{43}$" },
12415
+ path: { type: "string", pattern: "^/api/v2/automation-hooks/rcwh_[A-Za-z0-9_-]{24,80}$" }
12416
+ }
12417
+ },
12418
+ SessionAutomationActivity: {
12419
+ type: "object",
12420
+ required: ["id", "automationId", "triggerId", "source", "status", "invocationId", "createdAt", "updatedAt"],
12421
+ additionalProperties: false,
12422
+ properties: {
12423
+ id: { type: "string", minLength: 1, maxLength: 256 },
12424
+ automationId: { type: "string", minLength: 1, maxLength: 256 },
12425
+ triggerId: { type: "string", minLength: 1, maxLength: 256 },
12426
+ source: { enum: ["schedule", "webhook"] },
12427
+ status: { enum: ["queued", "started", "failed", "missed", "expired"] },
12428
+ invocationId: { type: "string", minLength: 1, maxLength: 256 },
12429
+ scheduledFor: { type: "integer", minimum: 0 },
12430
+ missedCount: { type: "integer", minimum: 1 },
12431
+ runId: { type: "string", minLength: 1, maxLength: 256 },
12432
+ failureCode: { type: "string", pattern: "^[A-Z][A-Z0-9_]{0,79}$" },
12433
+ createdAt: { type: "integer", minimum: 0 },
12434
+ updatedAt: { type: "integer", minimum: 0 }
12435
+ }
12436
+ },
11871
12437
  SessionAutomationRun: {
11872
12438
  description: "Privacy-bounded run state with its exact execution binding; terminal transcript, instruction, and provider detail are omitted.",
11873
12439
  type: "object",
@@ -13091,7 +13657,7 @@ function createPluginRuntime(options) {
13091
13657
  }
13092
13658
 
13093
13659
  // src/input-lease.ts
13094
- import { randomUUID as randomUUID5 } from "crypto";
13660
+ import { randomUUID as randomUUID6 } from "crypto";
13095
13661
  var INPUT_LEASE_TTL_MS = 3e4;
13096
13662
  function copy(lease) {
13097
13663
  return { ...lease };
@@ -13118,7 +13684,7 @@ var InputLeaseCoordinator = class {
13118
13684
  throw new Error("invalid input lease TTL");
13119
13685
  }
13120
13686
  this.now = options.now ?? Date.now;
13121
- this.generateId = options.generateId ?? randomUUID5;
13687
+ this.generateId = options.generateId ?? randomUUID6;
13122
13688
  this.scheduleExpiry = options.scheduleExpiry ?? true;
13123
13689
  this.onEvent = options.onEvent;
13124
13690
  }
@@ -14789,7 +15355,7 @@ async function verifyPeerConnection(input) {
14789
15355
  }
14790
15356
 
14791
15357
  // src/presence.ts
14792
- import { randomUUID as randomUUID6 } from "crypto";
15358
+ import { randomUUID as randomUUID7 } from "crypto";
14793
15359
  var PRESENCE_TTL_MS = 45e3;
14794
15360
  var PRESENCE_HEARTBEAT_MS = 15e3;
14795
15361
  function clone6(record) {
@@ -14833,7 +15399,7 @@ var PresenceCoordinator = class {
14833
15399
  throw new Error("invalid presence actor limit");
14834
15400
  }
14835
15401
  this.now = options.now ?? Date.now;
14836
- this.generateId = options.generateId ?? randomUUID6;
15402
+ this.generateId = options.generateId ?? randomUUID7;
14837
15403
  if (options.scheduleExpiry ?? true) {
14838
15404
  this.timer = setInterval(() => this.sweep(), Math.min(15e3, Math.max(1e3, Math.floor(this.ttlMs / 2))));
14839
15405
  this.timer.unref?.();
@@ -15199,9 +15765,9 @@ function cloudStatusResponse(status) {
15199
15765
  };
15200
15766
  }
15201
15767
  const authorization = status.authorization;
15202
- const syncFailures = status.authorizationFailures > 0 || status.heartbeatFailures > 0;
15768
+ const syncFailures = status.authorizationFailures > 0 || status.heartbeatFailures > 0 || status.automationFailures > 0;
15203
15769
  const syncState = authorization.status === "expired" ? "expired" : authorization.status === "pending" ? "pending" : authorization.status === "unavailable" ? syncFailures ? "degraded" : "syncing" : syncFailures ? "degraded" : "healthy";
15204
- const action = status.authorizationIssue === "credential-rejected" || status.authorizationIssue === "trust-expired" ? "reauthorize-host" : status.authorizationIssue === "invalid-control-plane-response" || status.authorizationIssue === "authorization-verification-failed" ? "contact-organization-admin" : status.authorizationIssue === "connectivity" ? "check-host-connectivity" : status.heartbeatFailures > 0 ? "check-host-connectivity" : authorization.status === "unavailable" ? "wait-for-cloud-sync" : authorization.status === "pending" ? "wait-for-authorization-activation" : authorization.status === "expired" ? "check-host-connectivity" : "none";
15770
+ const action = status.authorizationIssue === "credential-rejected" || status.authorizationIssue === "trust-expired" ? "reauthorize-host" : status.authorizationIssue === "invalid-control-plane-response" || status.authorizationIssue === "authorization-verification-failed" ? "contact-organization-admin" : status.authorizationIssue === "connectivity" ? "check-host-connectivity" : status.heartbeatFailures > 0 || status.automationFailures > 0 ? "check-host-connectivity" : authorization.status === "unavailable" ? "wait-for-cloud-sync" : authorization.status === "pending" ? "wait-for-authorization-activation" : authorization.status === "expired" ? "check-host-connectivity" : "none";
15205
15771
  return {
15206
15772
  v: 1,
15207
15773
  mode: "managed",
@@ -15638,6 +16204,7 @@ function createServer(config, deps = {}) {
15638
16204
  enabled: config.rateLimitRpm > 0
15639
16205
  });
15640
16206
  const pairingRateLimiter = new RateLimiter({ capacity: 30, windowMs: 6e4, burst: 10 });
16207
+ const automationWebhookRateLimiter = new RateLimiter({ capacity: 120, windowMs: 6e4, burst: 30 });
15641
16208
  const fsService = new FsService({ root: config.fsRoot });
15642
16209
  const terminalSharedRoot = terminalSharedBase({ dataDir, fsRoot: config.fsRoot });
15643
16210
  const backfilledFileSessions = /* @__PURE__ */ new Set();
@@ -15657,7 +16224,7 @@ function createServer(config, deps = {}) {
15657
16224
  const now = Date.now();
15658
16225
  const media = attachmentMedia(file.filename);
15659
16226
  store.putFile({
15660
- id: randomUUID7(),
16227
+ id: randomUUID8(),
15661
16228
  sessionId,
15662
16229
  direction: "sent",
15663
16230
  storage: "managed",
@@ -15729,6 +16296,7 @@ function createServer(config, deps = {}) {
15729
16296
  }
15730
16297
  const relayInternalCapability = randomBytes11(32).toString("base64url");
15731
16298
  const authenticatedPrincipals = /* @__PURE__ */ new WeakMap();
16299
+ const automationWebhookRequests = /* @__PURE__ */ new WeakMap();
15732
16300
  const hostPrincipal = () => ({
15733
16301
  actorType: config.accessToken ? "host" : "local",
15734
16302
  actorId: commandStore.getHost().id,
@@ -15875,6 +16443,28 @@ function createServer(config, deps = {}) {
15875
16443
  if (request.is404 && isPublicPath(path)) return;
15876
16444
  }
15877
16445
  if (path === "/health") return;
16446
+ const automationHookMatch = /^\/api\/v2\/automation-hooks\/(rcwh_[A-Za-z0-9_-]{24,80})$/.exec(path);
16447
+ if (request.method === "POST" && automationHookMatch && !hasEncodedSep(request.url)) {
16448
+ const limit = automationWebhookRateLimiter.take(request.ip);
16449
+ if (!limit.allowed) {
16450
+ reply.header("retry-after", String(limit.retryAfterSeconds)).code(429).send({ error: "rate limited" });
16451
+ return;
16452
+ }
16453
+ const hookId = automationHookMatch[1];
16454
+ const match = sessionAutomationStore.list().flatMap((automation) => automation.triggers.map((trigger) => ({ automation, trigger }))).find(
16455
+ (entry) => entry.automation.enabled && entry.trigger.type === "webhook" && entry.trigger.enabled && entry.trigger.hookId === hookId
16456
+ );
16457
+ const secret = extractBearerToken(request.headers.authorization);
16458
+ const presented = secret ? createHash7("sha256").update(secret).digest() : Buffer.alloc(0);
16459
+ const expected = match?.trigger.type === "webhook" ? Buffer.from(match.trigger.secretHash, "hex") : Buffer.alloc(32);
16460
+ if (!match || presented.length !== expected.length || !timingSafeEqual4(presented, expected)) {
16461
+ reply.code(401).send({ error: "unauthorized" });
16462
+ return;
16463
+ }
16464
+ automationWebhookRequests.set(request, match);
16465
+ authenticatedPrincipals.set(request, hostPrincipal());
16466
+ return;
16467
+ }
15878
16468
  if (request.method === "POST" && path === "/pairing/claim" && !hasEncodedSep(request.url)) {
15879
16469
  const originAllowed2 = isOriginAllowed(request.headers.origin, request.headers.host, {
15880
16470
  publicUrl: config.publicUrl,
@@ -16265,7 +16855,7 @@ function createServer(config, deps = {}) {
16265
16855
  return;
16266
16856
  }
16267
16857
  const principal = authenticatedPrincipals.get(request) ?? hostPrincipal();
16268
- const holderId = `ws:${randomUUID7()}`;
16858
+ const holderId = `ws:${randomUUID8()}`;
16269
16859
  let leaseId;
16270
16860
  let lastLeaseRevision = 0;
16271
16861
  const sendLeaseState = (reason, revision) => {
@@ -16560,7 +17150,7 @@ function createServer(config, deps = {}) {
16560
17150
  reply.code(400).send({ code: "INVALID_PROVIDER", error: "Invalid provider" });
16561
17151
  return;
16562
17152
  }
16563
- const id = requestedSessionId ?? randomUUID7();
17153
+ const id = requestedSessionId ?? randomUUID8();
16564
17154
  const existingMeta = requestedSessionId ? terminalManager.get(id) : void 0;
16565
17155
  if (!terminalAvailable) {
16566
17156
  reply.code(400).send({
@@ -17227,7 +17817,7 @@ function createServer(config, deps = {}) {
17227
17817
  const peerIdempotencyKey = (request, peerId) => {
17228
17818
  const actor = actorForRequest(request);
17229
17819
  const provided = request.headers["idempotency-key"];
17230
- const key = typeof provided === "string" ? provided : randomUUID7();
17820
+ const key = typeof provided === "string" ? provided : randomUUID8();
17231
17821
  return `peer-${createHash7("sha256").update(`${peerId}\0${actor.actorType}\0${actor.actorId}\0${key}`).digest("base64url")}`;
17232
17822
  };
17233
17823
  const workspaceAllowedByPeer = (peer, workspaceId) => typeof workspaceId === "string" && (peer.allowedWorkspaceIds === null || peer.allowedWorkspaceIds.includes(workspaceId));
@@ -19318,7 +19908,7 @@ data: ${JSON.stringify(data)}
19318
19908
  });
19319
19909
  return;
19320
19910
  }
19321
- holderId = `api-once:${randomUUID7()}`;
19911
+ holderId = `api-once:${randomUUID8()}`;
19322
19912
  const acquired = inputLeases.acquire(request.params.id, holderId, principal);
19323
19913
  if (acquired.status === "denied") {
19324
19914
  reply.code(409).send({ code: "INPUT_LEASE_REQUIRED", error: "terminal input is already controlled" });
@@ -19472,7 +20062,7 @@ data: ${JSON.stringify(data)}
19472
20062
  return;
19473
20063
  }
19474
20064
  const isImage = body.kind === "image" ? true : body.kind === "file" ? false : described.isImage;
19475
- const id = randomUUID7();
20065
+ const id = randomUUID8();
19476
20066
  const now = Date.now();
19477
20067
  const media = attachmentMedia(described.name);
19478
20068
  const stored2 = {
@@ -19926,9 +20516,78 @@ data: ${JSON.stringify(data)}
19926
20516
  const owner = currentNodeOwner();
19927
20517
  return automation && automation.owner.type === owner.type && automation.owner.id === owner.id ? automation : void 0;
19928
20518
  };
20519
+ const projectAutomationDefinition = (automation) => ({
20520
+ ...automation,
20521
+ triggers: automation.triggers.map((trigger) => {
20522
+ if (trigger.type !== "webhook") return trigger;
20523
+ const { secretHash, ...publicTrigger } = trigger;
20524
+ void secretHash;
20525
+ return publicTrigger;
20526
+ })
20527
+ });
20528
+ const newTriggerId = () => `rct_${randomBytes11(12).toString("base64url")}`;
20529
+ const newWebhookHookId = () => `rcwh_${randomBytes11(24).toString("base64url")}`;
20530
+ const newWebhookSecret = () => `rcws_${randomBytes11(32).toString("base64url")}`;
20531
+ const prepareAutomationTriggers = (value, current) => {
20532
+ if (!Array.isArray(value) || value.length > 16) throw new Error("invalid automation triggers");
20533
+ const existing = new Map(current?.triggers.map((trigger) => [trigger.id, trigger]) ?? []);
20534
+ const ids = /* @__PURE__ */ new Set();
20535
+ const webhookSecrets = [];
20536
+ const triggers = value.map((candidate) => {
20537
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
20538
+ throw new Error("invalid automation trigger");
20539
+ }
20540
+ const raw = candidate;
20541
+ const requestedId = typeof raw.id === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(raw.id) ? raw.id : void 0;
20542
+ const id = requestedId ?? newTriggerId();
20543
+ if (ids.has(id)) throw new Error("duplicate automation trigger id");
20544
+ ids.add(id);
20545
+ if (typeof raw.enabled !== "boolean") throw new Error("invalid automation trigger state");
20546
+ if (raw.type === "schedule") {
20547
+ if (typeof raw.cron !== "string" || typeof raw.timeZone !== "string" || raw.timeZone.length > 80 || raw.missedRunPolicy !== "skip" || Object.keys(raw).some(
20548
+ (key) => !["id", "type", "enabled", "cron", "timeZone", "missedRunPolicy"].includes(key)
20549
+ )) {
20550
+ throw new Error("invalid schedule trigger");
20551
+ }
20552
+ new Intl.DateTimeFormat("en-US", { timeZone: raw.timeZone }).format(0);
20553
+ return {
20554
+ id,
20555
+ type: "schedule",
20556
+ enabled: raw.enabled,
20557
+ cron: validateCronExpression(raw.cron),
20558
+ timeZone: raw.timeZone,
20559
+ missedRunPolicy: "skip"
20560
+ };
20561
+ }
20562
+ if (raw.type === "webhook") {
20563
+ if (Object.keys(raw).some((key) => !["id", "type", "enabled", "hookId"].includes(key))) {
20564
+ throw new Error("invalid webhook trigger");
20565
+ }
20566
+ const previous = existing.get(id);
20567
+ if (previous?.type === "webhook") {
20568
+ if (raw.hookId !== void 0 && raw.hookId !== previous.hookId) {
20569
+ throw new Error("webhook identity cannot be replaced");
20570
+ }
20571
+ return { ...previous, enabled: raw.enabled };
20572
+ }
20573
+ const hookId = newWebhookHookId();
20574
+ const secret = newWebhookSecret();
20575
+ webhookSecrets.push({ triggerId: id, hookId, secret, path: `/api/v2/automation-hooks/${hookId}` });
20576
+ return {
20577
+ id,
20578
+ type: "webhook",
20579
+ enabled: raw.enabled,
20580
+ hookId,
20581
+ secretHash: createHash7("sha256").update(secret).digest("hex")
20582
+ };
20583
+ }
20584
+ throw new Error("invalid automation trigger");
20585
+ });
20586
+ return { triggers, webhookSecrets };
20587
+ };
19929
20588
  const automationInvocationIdentity = (request, automationId) => {
19930
20589
  const idempotency2 = mutationContexts.get(request)?.idempotency;
19931
- if (!idempotency2) return { invocationId: randomUUID7(), sessionId: randomUUID7() };
20590
+ if (!idempotency2) return { invocationId: randomUUID8(), sessionId: randomUUID8() };
19932
20591
  const actor = actorForRequest(request);
19933
20592
  const digest2 = createHash7("sha256").update("roamcode-automation-invocation-v1\0").update(JSON.stringify([actor.actorType, actor.actorId, idempotency2.key, idempotency2.fingerprint, automationId])).digest("hex");
19934
20593
  const uuidHex = digest2.slice(0, 32).split("");
@@ -20197,10 +20856,13 @@ data: ${JSON.stringify(data)}
20197
20856
  "cwd",
20198
20857
  "instruction",
20199
20858
  "runtimeOptions",
20200
- "trigger"
20859
+ "trigger",
20860
+ "triggers"
20201
20861
  ]);
20202
20862
  const automationUpdateKeys = /* @__PURE__ */ new Set([...automationCreateKeys, "expectedRevision"]);
20203
- app.get("/api/v2/automations", async () => ({ automations: sessionAutomationStore.list(currentNodeOwner()) }));
20863
+ app.get("/api/v2/automations", async () => ({
20864
+ automations: sessionAutomationStore.list(currentNodeOwner()).map(projectAutomationDefinition)
20865
+ }));
20204
20866
  app.post("/api/v2/automations", { bodyLimit: 128 * 1024 }, async (request, reply) => {
20205
20867
  if (!validObjectBody(request.body)) return sendInvalidAutomation(reply);
20206
20868
  if ("owner" in request.body || "provider" in request.body) {
@@ -20220,15 +20882,17 @@ data: ${JSON.stringify(data)}
20220
20882
  );
20221
20883
  if (!target) return;
20222
20884
  try {
20885
+ const prepared = prepareAutomationTriggers(request.body.triggers ?? [], void 0);
20223
20886
  const automation = sessionAutomationStore.create({
20224
20887
  owner: currentNodeOwner(),
20225
20888
  name: request.body.name,
20226
20889
  ...request.body.enabled === void 0 ? {} : { enabled: request.body.enabled },
20227
20890
  ...target,
20228
20891
  instruction: request.body.instruction,
20229
- ...request.body.trigger === void 0 ? { trigger: { type: "manual" } } : { trigger: request.body.trigger }
20892
+ ...request.body.trigger === void 0 ? { trigger: { type: "manual" } } : { trigger: request.body.trigger },
20893
+ triggers: prepared.triggers
20230
20894
  });
20231
- reply.code(201).send({ automation });
20895
+ reply.code(201).send({ automation: projectAutomationDefinition(automation), webhookSecrets: prepared.webhookSecrets });
20232
20896
  } catch {
20233
20897
  sendInvalidAutomation(reply);
20234
20898
  }
@@ -20239,7 +20903,7 @@ data: ${JSON.stringify(data)}
20239
20903
  reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
20240
20904
  return;
20241
20905
  }
20242
- return { automation };
20906
+ return { automation: projectAutomationDefinition(automation) };
20243
20907
  });
20244
20908
  app.patch(
20245
20909
  "/api/v2/automations/:automationId",
@@ -20277,18 +20941,20 @@ data: ${JSON.stringify(data)}
20277
20941
  ...target
20278
20942
  };
20279
20943
  try {
20944
+ const prepared = prepareAutomationTriggers(request.body.triggers ?? current.triggers, current);
20945
+ input.triggers = prepared.triggers;
20280
20946
  const automation = sessionAutomationStore.update(current.id, input, request.body.expectedRevision);
20281
20947
  if (!automation) {
20282
20948
  reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
20283
20949
  return;
20284
20950
  }
20285
- return { automation };
20951
+ return { automation: projectAutomationDefinition(automation), webhookSecrets: prepared.webhookSecrets };
20286
20952
  } catch (error) {
20287
20953
  if (error instanceof SessionAutomationRevisionConflictError) {
20288
20954
  reply.code(409).send({
20289
20955
  code: "SESSION_AUTOMATION_REVISION_CONFLICT",
20290
20956
  error: "automation state changed",
20291
- current: error.current
20957
+ current: projectAutomationDefinition(error.current)
20292
20958
  });
20293
20959
  return;
20294
20960
  }
@@ -20303,6 +20969,70 @@ data: ${JSON.stringify(data)}
20303
20969
  }
20304
20970
  reply.code(204).send();
20305
20971
  });
20972
+ app.get(
20973
+ "/api/v2/automations/:automationId/activity",
20974
+ async (request, reply) => {
20975
+ const automation = ownedAutomationIncludingRemoved(request.params.automationId);
20976
+ if (!automation) {
20977
+ reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
20978
+ return;
20979
+ }
20980
+ const limit = request.query.limit === void 0 ? 25 : Number(request.query.limit);
20981
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
20982
+ reply.code(400).send({ code: "INVALID_AUTOMATION_ACTIVITY_LIMIT", error: "limit must be between 1 and 100" });
20983
+ return;
20984
+ }
20985
+ return { activities: sessionAutomationStore.listActivities(automation.id, limit) };
20986
+ }
20987
+ );
20988
+ app.post(
20989
+ "/api/v2/automations/:automationId/triggers/:triggerId/secret",
20990
+ { bodyLimit: 1024 },
20991
+ async (request, reply) => {
20992
+ const current = ownedAutomation(request.params.automationId);
20993
+ if (!current) {
20994
+ reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
20995
+ return;
20996
+ }
20997
+ if (!validObjectBody(request.body) || !hasOnlyKeys(request.body, /* @__PURE__ */ new Set(["expectedRevision"])) || !Number.isSafeInteger(request.body.expectedRevision) || request.body.expectedRevision !== current.revision) {
20998
+ return sendInvalidAutomation(reply, "a current expectedRevision is required");
20999
+ }
21000
+ const selected = current.triggers.find(
21001
+ (trigger) => trigger.id === request.params.triggerId && trigger.type === "webhook"
21002
+ );
21003
+ if (!selected || selected.type !== "webhook") {
21004
+ reply.code(404).send({ code: "AUTOMATION_TRIGGER_NOT_FOUND", error: "webhook trigger not found" });
21005
+ return;
21006
+ }
21007
+ const secret = newWebhookSecret();
21008
+ const triggers = current.triggers.map(
21009
+ (trigger) => trigger.id === selected.id ? { ...selected, secretHash: createHash7("sha256").update(secret).digest("hex") } : trigger
21010
+ );
21011
+ try {
21012
+ const automation = sessionAutomationStore.update(current.id, { triggers }, current.revision);
21013
+ if (!automation) throw new Error("automation disappeared");
21014
+ return {
21015
+ automation: projectAutomationDefinition(automation),
21016
+ webhookSecret: {
21017
+ triggerId: selected.id,
21018
+ hookId: selected.hookId,
21019
+ secret,
21020
+ path: `/api/v2/automation-hooks/${selected.hookId}`
21021
+ }
21022
+ };
21023
+ } catch (error) {
21024
+ if (error instanceof SessionAutomationRevisionConflictError) {
21025
+ reply.code(409).send({
21026
+ code: "SESSION_AUTOMATION_REVISION_CONFLICT",
21027
+ error: "automation state changed",
21028
+ current: projectAutomationDefinition(error.current)
21029
+ });
21030
+ return;
21031
+ }
21032
+ sendInvalidAutomation(reply);
21033
+ }
21034
+ }
21035
+ );
20306
21036
  app.get(
20307
21037
  "/api/v2/automations/:automationId/runs",
20308
21038
  async (request, reply) => {
@@ -20531,6 +21261,46 @@ data: ${JSON.stringify(data)}
20531
21261
  );
20532
21262
  }
20533
21263
  );
21264
+ const parsedAutomationConcurrency = Number.parseInt(process.env.ROAMCODE_AUTOMATION_CONCURRENCY ?? "2", 10);
21265
+ const automationTriggerEngine = createAutomationTriggerEngine({
21266
+ store: sessionAutomationStore,
21267
+ concurrency: Number.isSafeInteger(parsedAutomationConcurrency) && parsedAutomationConcurrency > 0 ? parsedAutomationConcurrency : 2,
21268
+ execute: async (activity) => {
21269
+ const response2 = await app.inject({
21270
+ method: "POST",
21271
+ url: `/api/v2/automations/${encodeURIComponent(activity.automationId)}/runs`,
21272
+ headers: {
21273
+ "idempotency-key": activity.id,
21274
+ ...config.accessToken ? { authorization: `Bearer ${config.accessToken}` } : {}
21275
+ }
21276
+ });
21277
+ const body = (() => {
21278
+ try {
21279
+ return response2.json();
21280
+ } catch {
21281
+ return {};
21282
+ }
21283
+ })();
21284
+ if (response2.statusCode !== 201 || typeof body.run?.id !== "string") {
21285
+ throw new Error("automation trigger execution failed");
21286
+ }
21287
+ return { runId: body.run.id };
21288
+ }
21289
+ });
21290
+ app.post(
21291
+ "/api/v2/automation-hooks/:hookId",
21292
+ { bodyLimit: 64 * 1024 },
21293
+ async (request, reply) => {
21294
+ const authorized = automationWebhookRequests.get(request);
21295
+ if (!authorized || authorized.trigger.type !== "webhook" || authorized.trigger.hookId !== request.params.hookId) {
21296
+ reply.code(401).send({ error: "unauthorized" });
21297
+ return;
21298
+ }
21299
+ automationTriggerEngine.enqueueWebhook(authorized.automation, authorized.trigger);
21300
+ reply.code(202).send({ accepted: true });
21301
+ }
21302
+ );
21303
+ app.addHook("onReady", async () => automationTriggerEngine.start());
20534
21304
  app.get("/providers", async () => {
20535
21305
  return { providers: await readProviderAvailability() };
20536
21306
  });
@@ -20889,7 +21659,7 @@ data: ${JSON.stringify(data)}
20889
21659
  reply.code(400).send({ error: "no file field in the upload" });
20890
21660
  return;
20891
21661
  }
20892
- const id = randomUUID7();
21662
+ const id = randomUUID8();
20893
21663
  try {
20894
21664
  const dir = await fsService.ensureDirWithinRoot(
20895
21665
  `${terminalSharedDir({ dataDir, fsRoot: config.fsRoot, sessionId })}/${id}`
@@ -20941,7 +21711,7 @@ data: ${JSON.stringify(data)}
20941
21711
  reply.code(400).send({ error: "an edited image is required" });
20942
21712
  return;
20943
21713
  }
20944
- const id = randomUUID7();
21714
+ const id = randomUUID8();
20945
21715
  try {
20946
21716
  const dir = await fsService.ensureDirWithinRoot(
20947
21717
  `${terminalSharedDir({ dataDir, fsRoot: config.fsRoot, sessionId: source.sessionId })}/${id}`
@@ -21067,6 +21837,7 @@ data: ${JSON.stringify(data)}
21067
21837
  );
21068
21838
  if (deps.webDir) registerStatic(app, { webDir: deps.webDir });
21069
21839
  app.addHook("onClose", async () => {
21840
+ automationTriggerEngine.stop();
21070
21841
  clearInterval(sharedSweepTimer);
21071
21842
  inputLeases.close();
21072
21843
  unsubscribeAutomations();
@@ -21162,6 +21933,25 @@ data: ${JSON.stringify(data)}
21162
21933
  authGate,
21163
21934
  terminalManager,
21164
21935
  terminalAvailable,
21936
+ automationWebhookRegistrations: () => sessionAutomationStore.list().flatMap(
21937
+ (automation) => automation.triggers.filter((trigger) => trigger.type === "webhook").map((trigger) => ({
21938
+ hookId: trigger.hookId,
21939
+ automationId: automation.id,
21940
+ triggerId: trigger.id,
21941
+ secretHash: trigger.secretHash,
21942
+ enabled: automation.enabled && trigger.enabled
21943
+ }))
21944
+ ),
21945
+ acceptCloudAutomationInvocation: async (invocation) => {
21946
+ const automation = sessionAutomationStore.get(invocation.automationId);
21947
+ const trigger = automation?.triggers.find(
21948
+ (candidate) => candidate.type === "webhook" && candidate.id === invocation.triggerId && candidate.hookId === invocation.hookId
21949
+ );
21950
+ if (!automation?.enabled || !trigger?.enabled || trigger.type !== "webhook") {
21951
+ throw new Error("managed automation webhook is no longer active");
21952
+ }
21953
+ automationTriggerEngine.enqueueWebhook(automation, trigger, invocation.id);
21954
+ },
21165
21955
  inputLeases,
21166
21956
  teamStore,
21167
21957
  policyStore,
@@ -21581,7 +22371,7 @@ function createUsageService(opts) {
21581
22371
 
21582
22372
  // src/claude-auth-service.ts
21583
22373
  import { spawn as nodeSpawn2 } from "child_process";
21584
- import { randomUUID as randomUUID8 } from "crypto";
22374
+ import { randomUUID as randomUUID9 } from "crypto";
21585
22375
  function parseAuthStatus(stdout) {
21586
22376
  try {
21587
22377
  const o = JSON.parse(stdout);
@@ -21658,7 +22448,7 @@ var ClaudeAuthService = class {
21658
22448
  reject(err instanceof Error ? err : new Error("failed to spawn claude"));
21659
22449
  return;
21660
22450
  }
21661
- const id = randomUUID8();
22451
+ const id = randomUUID9();
21662
22452
  let settled = false;
21663
22453
  let buf = "";
21664
22454
  const onData = (d) => {
@@ -23020,7 +23810,7 @@ var CodexMetadataService = class {
23020
23810
 
23021
23811
  // src/providers/claude-metadata-service.ts
23022
23812
  import { execFile as nodeExecFile2, spawn as nodeSpawn3 } from "child_process";
23023
- import { randomUUID as randomUUID9 } from "crypto";
23813
+ import { randomUUID as randomUUID10 } from "crypto";
23024
23814
  import { StringDecoder } from "string_decoder";
23025
23815
  var SAFE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:/\u005b\u005d-]*$/;
23026
23816
  var MAX_MODELS = 64;
@@ -23228,7 +24018,7 @@ function createClaudeMetadataRunner(options) {
23228
24018
  reject(metadataUnavailable2());
23229
24019
  return;
23230
24020
  }
23231
- const requestId = `roamcode-models-${randomUUID9()}`;
24021
+ const requestId = `roamcode-models-${randomUUID10()}`;
23232
24022
  const decoder = new StringDecoder("utf8");
23233
24023
  let stdoutBuffer = "";
23234
24024
  let outputBytes = 0;
@@ -23480,7 +24270,7 @@ var CodexLatestService = class {
23480
24270
  import { execFile } from "child_process";
23481
24271
  import { constants as constants5 } from "fs";
23482
24272
  import { access as access2, chmod, copyFile, link, realpath as realpath7, rename as rename3, rm as rm2, rmdir as rmdir2, stat as stat7 } from "fs/promises";
23483
- import { randomUUID as randomUUID10 } from "crypto";
24273
+ import { randomUUID as randomUUID11 } from "crypto";
23484
24274
  import { basename as basename5, delimiter, dirname as dirname8, isAbsolute as isAbsolute9, join as join14, sep as sep5 } from "path";
23485
24275
  var CODEX_EXECUTABLE_PROBE_TIMEOUT_MS = 5e3;
23486
24276
  var OPENAI_CODE_SIGNING_TEAM_ID = "2DC432GLL2";
@@ -23590,7 +24380,7 @@ async function removeLegacyManagedCopy(dataDir) {
23590
24380
  }
23591
24381
  async function repairHomebrewExecutable(source, sourceStat, env, deps) {
23592
24382
  if (!await deps.verifyOfficialSignature(source)) return false;
23593
- const nonce = randomUUID10();
24383
+ const nonce = randomUUID11();
23594
24384
  const directory = dirname8(source);
23595
24385
  const name = basename5(source);
23596
24386
  const temporary = join14(directory, `.${name}.roamcode-repair-${nonce}`);
@@ -24821,6 +25611,32 @@ var CloudHostHeartbeatV1Schema = z9.object({
24821
25611
  context.addIssue({ code: "custom", path: ["capabilities"], message: "capabilities must be unique" });
24822
25612
  }
24823
25613
  });
25614
+ var CloudAutomationWebhookRegistrationSchema = z9.object({
25615
+ hookId: z9.string().regex(/^rcwh_[A-Za-z0-9_-]{24,80}$/),
25616
+ automationId: SafeIdentifierSchema,
25617
+ triggerId: SafeIdentifierSchema,
25618
+ secretHash: z9.string().regex(/^[a-f0-9]{64}$/),
25619
+ enabled: z9.boolean()
25620
+ }).strict();
25621
+ var CloudAutomationInvocationSchema = z9.object({
25622
+ id: z9.uuid(),
25623
+ automationId: SafeIdentifierSchema,
25624
+ triggerId: SafeIdentifierSchema,
25625
+ hookId: z9.string().regex(/^rcwh_[A-Za-z0-9_-]{24,80}$/),
25626
+ createdAt: TimestampSchema
25627
+ }).strict();
25628
+ var CloudAutomationSyncRequestSchema = z9.object({
25629
+ v: z9.literal(1),
25630
+ organizationId: SafeIdentifierSchema,
25631
+ hostId: SafeIdentifierSchema,
25632
+ instanceId: SafeIdentifierSchema,
25633
+ registrations: z9.array(CloudAutomationWebhookRegistrationSchema).max(128).refine(
25634
+ (registrations) => new Set(registrations.map((registration) => registration.hookId)).size === registrations.length,
25635
+ "webhook identities must be unique"
25636
+ ),
25637
+ acknowledgements: z9.array(z9.uuid()).max(200).refine((values) => new Set(values).size === values.length)
25638
+ }).strict();
25639
+ var CloudAutomationSyncResponseSchema = z9.object({ invocations: z9.array(CloudAutomationInvocationSchema).max(50) }).strict();
24824
25640
  var CloudAuthorizationScopeV1Schema = z9.discriminatedUnion("type", [
24825
25641
  z9.object({ type: z9.literal("organization") }).strict(),
24826
25642
  z9.object({ type: z9.literal("host"), id: SafeIdentifierSchema }).strict(),
@@ -25859,6 +26675,7 @@ function openCloudAuthorizationStore(options) {
25859
26675
 
25860
26676
  // src/cloud-host-runtime.ts
25861
26677
  var CLOUD_HOST_HEARTBEAT_PATH = "/api/v1/hosts/heartbeat";
26678
+ var CLOUD_HOST_AUTOMATION_SYNC_PATH = "/api/v1/hosts/automation-sync";
25862
26679
  var CLOUD_HOST_AUTHORIZATION_SNAPSHOT_PATH = "/api/v1/hosts/authorization-snapshot";
25863
26680
  var CLOUD_HOST_MAX_SIGNED_RESPONSE_BYTES = 16 * 1024 * 1024;
25864
26681
  var DEFAULT_REQUEST_TIMEOUT_MS3 = 15e3;
@@ -25945,6 +26762,7 @@ function createCloudHostRuntime(options) {
25945
26762
  let lastHeartbeatAt;
25946
26763
  let lastAuthorizationAt;
25947
26764
  let heartbeatFailures = 0;
26765
+ let automationFailures = 0;
25948
26766
  let authorizationFailures = 0;
25949
26767
  let lastAuthorizationIssue;
25950
26768
  let running = false;
@@ -25954,6 +26772,8 @@ function createCloudHostRuntime(options) {
25954
26772
  let heartbeatInFlight;
25955
26773
  let authorizationInFlight;
25956
26774
  const capabilities2 = () => [...new Set(typeof options.capabilities === "function" ? options.capabilities() : options.capabilities)].sort();
26775
+ const automationWebhooks = () => (typeof options.automationWebhooks === "function" ? options.automationWebhooks() : options.automationWebhooks ?? []).map((registration) => CloudAutomationWebhookRegistrationSchema.parse(registration)).sort((left, right) => left.hookId.localeCompare(right.hookId));
26776
+ const automationAcknowledgements = /* @__PURE__ */ new Set();
25957
26777
  const request = async (path, init) => {
25958
26778
  try {
25959
26779
  return await fetchImpl(`${config.controlPlaneOrigin}${path}`, {
@@ -26073,6 +26893,51 @@ function createCloudHostRuntime(options) {
26073
26893
  }
26074
26894
  await response2.body?.cancel().catch(() => void 0);
26075
26895
  lastHeartbeatAt = currentNow;
26896
+ if (state === "ready" && options.automationWebhooks && options.onAutomationInvocation) {
26897
+ try {
26898
+ let deliveryFailed = false;
26899
+ for (let page = 0; page < 4; page += 1) {
26900
+ const acknowledgements = [...automationAcknowledgements].slice(0, 200);
26901
+ const payload = CloudAutomationSyncRequestSchema.parse({
26902
+ v: 1,
26903
+ organizationId: config.organizationId,
26904
+ hostId: config.hostId,
26905
+ instanceId: options.instanceId,
26906
+ registrations: automationWebhooks(),
26907
+ acknowledgements
26908
+ });
26909
+ const automationResponse = await request(CLOUD_HOST_AUTOMATION_SYNC_PATH, {
26910
+ method: "POST",
26911
+ headers: authenticatedHeaders({ "content-type": "application/json", accept: "application/json" }),
26912
+ body: JSON.stringify(payload)
26913
+ });
26914
+ if (automationResponse.status === 404) {
26915
+ await automationResponse.body?.cancel().catch(() => void 0);
26916
+ break;
26917
+ }
26918
+ if (automationResponse.status !== 200) {
26919
+ await boundedResponseText2(automationResponse).catch(() => "");
26920
+ throw responseError(automationResponse);
26921
+ }
26922
+ const parsed = CloudAutomationSyncResponseSchema.parse(
26923
+ JSON.parse(await boundedResponseText2(automationResponse))
26924
+ );
26925
+ for (const id of acknowledgements) automationAcknowledgements.delete(id);
26926
+ for (const invocation of parsed.invocations) {
26927
+ try {
26928
+ await options.onAutomationInvocation(invocation);
26929
+ automationAcknowledgements.add(invocation.id);
26930
+ } catch {
26931
+ deliveryFailed = true;
26932
+ }
26933
+ }
26934
+ if (parsed.invocations.length === 0 && automationAcknowledgements.size === 0) break;
26935
+ }
26936
+ automationFailures = deliveryFailed ? automationFailures + 1 : 0;
26937
+ } catch {
26938
+ automationFailures += 1;
26939
+ }
26940
+ }
26076
26941
  };
26077
26942
  const jittered = (baseMs) => {
26078
26943
  const sample = random();
@@ -26137,6 +27002,7 @@ function createCloudHostRuntime(options) {
26137
27002
  status: () => ({
26138
27003
  running,
26139
27004
  heartbeatFailures,
27005
+ automationFailures,
26140
27006
  authorizationFailures,
26141
27007
  ...lastAuthorizationIssue ? { authorizationIssue: lastAuthorizationIssue } : {},
26142
27008
  ...lastHeartbeatAt === void 0 ? {} : { lastHeartbeatAt },
@@ -26594,13 +27460,14 @@ function cloudHostCapabilities(input) {
26594
27460
  return [
26595
27461
  `authorization.v${input.authorizationVersion}`,
26596
27462
  ...input.terminalAvailable ? ["terminal.v1"] : [],
27463
+ ...input.terminalAvailable ? ["automation-schedule.v1", "automation-webhook.v1"] : [],
26597
27464
  ...input.relayEnabled ? ["relay.v1"] : [],
26598
27465
  ...input.managedDeviceEnrollmentEnabled && input.relayEnabled && input.terminalAvailable ? ["managed-device-enrollment.v1"] : []
26599
27466
  ];
26600
27467
  }
26601
27468
  async function startServer(env = process.env, options = {}) {
26602
27469
  const config = loadServerConfig(env);
26603
- const healthInstanceId = randomUUID11();
27470
+ const healthInstanceId = randomUUID12();
26604
27471
  ensureDataDir(config.dataDir);
26605
27472
  const loopback = isLoopbackAddress(config.bindAddress);
26606
27473
  let token;
@@ -26873,6 +27740,7 @@ async function startServer(env = process.env, options = {}) {
26873
27740
  cloudStatus: () => cloudHostRuntime?.status() ?? {
26874
27741
  running: false,
26875
27742
  heartbeatFailures: 0,
27743
+ automationFailures: 0,
26876
27744
  authorizationFailures: 0,
26877
27745
  authorization: cloudAuthorizationStore.getState()
26878
27746
  }
@@ -26905,6 +27773,8 @@ async function startServer(env = process.env, options = {}) {
26905
27773
  relayEnabled: relayConfig !== void 0,
26906
27774
  managedDeviceEnrollmentEnabled: cloudDeviceEnrollmentConfirmer !== void 0 && relayProvisioner !== void 0
26907
27775
  }),
27776
+ automationWebhooks: () => result.automationWebhookRegistrations(),
27777
+ onAutomationInvocation: (invocation) => result.acceptCloudAutomationInvocation(invocation),
26908
27778
  ...relayConfig ? {
26909
27779
  relayHostIdentity: {
26910
27780
  publicKey: relayConfig.hostIdentity.publicKey,