@agent-native/core 0.157.9 → 0.157.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +5 -2
  2. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +18 -6
  4. package/corpus/templates/analytics/server/plugins/db.ts +139 -0
  5. package/corpus/templates/slides/AGENTS.md +14 -10
  6. package/corpus/templates/slides/actions/export-pptx.ts +1215 -94
  7. package/corpus/templates/slides/actions/import-file.ts +16 -1
  8. package/corpus/templates/slides/actions/import-pptx.ts +61 -43
  9. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
  10. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
  11. package/corpus/templates/slides/app/global.css +19 -1
  12. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
  13. package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
  14. package/corpus/templates/slides/app/lib/export-pptx-client.ts +556 -25
  15. package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
  16. package/corpus/templates/slides/server/handlers/import/html-converter.ts +1176 -26
  17. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +13 -0
  18. package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
  19. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -4
  20. package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
  21. package/dist/agent/harness/migrations.js +24 -3
  22. package/dist/agent/production-agent.js +11 -0
  23. package/dist/agent/run-loop-with-resume.js +21 -4
  24. package/dist/automations/service.d.ts +6 -0
  25. package/dist/automations/service.js +35 -0
  26. package/dist/cli/code-agent-connector.d.ts +4 -1
  27. package/dist/cli/code-agent-connector.js +154 -23
  28. package/dist/cli/code-agent-executor.js +255 -13
  29. package/dist/cli/code-agent-runs.d.ts +1 -0
  30. package/dist/cli/code-agent-runs.js +2 -1
  31. package/dist/cli/portal-workspace.d.ts +49 -0
  32. package/dist/cli/portal-workspace.js +320 -0
  33. package/dist/client/AssistantChat.d.ts +1 -1
  34. package/dist/client/AssistantChat.js +39 -61
  35. package/dist/client/ConnectBuilderCard.js +33 -5
  36. package/dist/client/agent-chat-adapter.js +4 -0
  37. package/dist/client/analytics-platform.d.ts +2 -0
  38. package/dist/client/analytics-platform.js +12 -0
  39. package/dist/client/analytics.d.ts +3 -0
  40. package/dist/client/analytics.js +10 -1
  41. package/dist/client/chat/index.d.ts +1 -0
  42. package/dist/client/chat/index.js +1 -0
  43. package/dist/client/chat/run-recovery.d.ts +2 -1
  44. package/dist/client/chat/run-recovery.js +2 -2
  45. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  46. package/dist/client/chat-first/apps-rail.js +11 -11
  47. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  48. package/dist/client/chat-first/surface-tabs.js +38 -36
  49. package/dist/client/chat-first/types.d.ts +5 -0
  50. package/dist/client/chat-first.d.ts +1 -1
  51. package/dist/client/chat-first.js +4 -4
  52. package/dist/client/desktop-local-code-change.d.ts +10 -0
  53. package/dist/client/desktop-local-code-change.js +16 -0
  54. package/dist/client/index.d.ts +1 -0
  55. package/dist/client/index.js +1 -0
  56. package/dist/client/observability/ThumbsFeedback.js +21 -7
  57. package/dist/client/terminal/AgentTerminal.d.ts +9 -1
  58. package/dist/client/terminal/AgentTerminal.js +38 -3
  59. package/dist/client/terminal/index.d.ts +1 -1
  60. package/dist/client/terminal/index.js +1 -1
  61. package/dist/client/track.js +9 -1
  62. package/dist/client/use-action.js +3 -0
  63. package/dist/collab/awareness.d.ts +2 -2
  64. package/dist/collab/struct-routes.d.ts +1 -1
  65. package/dist/db/client.js +1 -0
  66. package/dist/deploy/build.js +1 -1
  67. package/dist/ingestion/pptx.d.ts +78 -0
  68. package/dist/ingestion/pptx.js +991 -107
  69. package/dist/integrations/index.d.ts +3 -3
  70. package/dist/integrations/index.js +2 -2
  71. package/dist/integrations/plugin.js +95 -6
  72. package/dist/integrations/remote-commands-store.d.ts +5 -0
  73. package/dist/integrations/remote-commands-store.js +63 -22
  74. package/dist/integrations/remote-devices-store.d.ts +2 -1
  75. package/dist/integrations/remote-devices-store.js +46 -0
  76. package/dist/integrations/remote-types.d.ts +20 -0
  77. package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
  78. package/dist/jobs/actions/list-recurring-jobs.js +5 -1
  79. package/dist/jobs/frontmatter.d.ts +16 -0
  80. package/dist/jobs/frontmatter.js +53 -0
  81. package/dist/jobs/remote-execution.d.ts +42 -0
  82. package/dist/jobs/remote-execution.js +287 -0
  83. package/dist/jobs/scheduler.js +102 -3
  84. package/dist/jobs/tools.js +50 -3
  85. package/dist/notifications/routes.d.ts +4 -4
  86. package/dist/observability/routes.d.ts +5 -5
  87. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  88. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  89. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  90. package/dist/resources/handlers.d.ts +1 -1
  91. package/dist/secrets/routes.d.ts +9 -9
  92. package/dist/server/action-routes.js +4 -2
  93. package/dist/server/agent-chat-plugin.js +6 -0
  94. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  95. package/dist/server/agent-run-context.d.ts +1 -0
  96. package/dist/server/agent-run-context.js +9 -0
  97. package/dist/server/core-routes-plugin.js +8 -1
  98. package/dist/server/request-context.d.ts +2 -0
  99. package/dist/server/transcribe-voice.d.ts +1 -1
  100. package/dist/shared/analytics-platform.d.ts +6 -0
  101. package/dist/shared/analytics-platform.js +14 -0
  102. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  103. package/dist/shared/mcp-embed-headers.js +1 -1
  104. package/dist/styles/agent-native.css +83 -103
  105. package/dist/terminal/cli-registry.js +5 -0
  106. package/dist/tracking/registry.js +9 -1
  107. package/dist/triggers/actions/list-automations.d.ts +3 -0
  108. package/dist/triggers/actions/list-automations.js +3 -0
  109. package/dist/triggers/actions/manage-automation.d.ts +9 -0
  110. package/dist/triggers/actions/manage-automation.js +14 -2
  111. package/dist/triggers/actions.js +82 -7
  112. package/docs/content/template-content-developers.mdx +48 -0
  113. package/docs/content/toolkit-ui.mdx +57 -0
  114. package/package.json +2 -2
@@ -0,0 +1,287 @@
1
+ import { enqueueRemoteCommand, getRemoteCommand, getRemoteCommandByIdempotencyKey, } from "../integrations/remote-commands-store.js";
2
+ import { getRemoteDeviceForOwner, getRemoteExecutionCapabilities, } from "../integrations/remote-devices-store.js";
3
+ import { organizationResourceOwner, resourceGetByPath, resourcePutIfCurrent, } from "../resources/store.js";
4
+ import { buildJobResourceContent, parseJobResource, } from "./frontmatter.js";
5
+ import { finishAutomationRun, startAutomationRun } from "./run-history.js";
6
+ /** A remote run is allowed to be long-lived, but never indefinitely invisible. */
7
+ export const REMOTE_AUTOMATION_MAX_ACTIVE_MS = 24 * 60 * 60_000;
8
+ function isRecord(value) {
9
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ function readString(...values) {
12
+ for (const value of values) {
13
+ if (typeof value === "string" && value.trim())
14
+ return value.trim();
15
+ }
16
+ return undefined;
17
+ }
18
+ function automationName(path) {
19
+ return path.replace(/^jobs\//, "").replace(/\.md$/, "");
20
+ }
21
+ function remoteRequestId(resource, meta, now) {
22
+ const runAt = meta.lastRun || now.toISOString();
23
+ return `remote-automation:${resource.owner}:${resource.path}:${runAt}`.slice(0, 512);
24
+ }
25
+ function assertRemoteHostCanRun(device, meta) {
26
+ if (device.status !== "active") {
27
+ throw new Error(`Execution host "${device.label}" is revoked or inactive.`);
28
+ }
29
+ const capabilities = getRemoteExecutionCapabilities(device);
30
+ if (!capabilities)
31
+ return;
32
+ if (capabilities.acceptsScheduledWork !== true) {
33
+ throw new Error(`Execution host "${device.label}" does not accept scheduled work.`);
34
+ }
35
+ if (capabilities.workloads?.length &&
36
+ !capabilities.workloads.includes("scheduled-code")) {
37
+ throw new Error(`Execution host "${device.label}" does not advertise scheduled-code work.`);
38
+ }
39
+ if (meta.executionEngine &&
40
+ capabilities.engines?.length &&
41
+ !capabilities.engines.includes(meta.executionEngine)) {
42
+ throw new Error(`Execution host "${device.label}" does not advertise engine "${meta.executionEngine}".`);
43
+ }
44
+ }
45
+ async function resolveRemoteHost(input) {
46
+ const device = await getRemoteDeviceForOwner({
47
+ id: input.hostId,
48
+ ownerEmail: input.ownerEmail,
49
+ orgId: input.orgId,
50
+ });
51
+ if (!device) {
52
+ throw new Error(`Execution host "${input.hostId}" is not paired with this account and organization.`);
53
+ }
54
+ return device;
55
+ }
56
+ function remoteRunIdFromCommand(command) {
57
+ const result = isRecord(command.result) ? command.result : undefined;
58
+ const nested = isRecord(result?.result) ? result.result : result;
59
+ const run = isRecord(nested?.run) ? nested.run : undefined;
60
+ return readString(run?.id, run?.runId);
61
+ }
62
+ /**
63
+ * Queue one host-targeted automation. The request key is derived from the
64
+ * persisted start timestamp, so a worker crash between the queue write and
65
+ * resource bookkeeping cannot create a second remote run.
66
+ */
67
+ export async function dispatchRemoteAutomation(input) {
68
+ const now = input.now ?? new Date();
69
+ const hostId = input.meta.executionHostId?.trim();
70
+ if (!hostId)
71
+ throw new Error("A remote automation requires executionHostId.");
72
+ const device = await resolveRemoteHost({
73
+ hostId,
74
+ ownerEmail: input.ownerEmail,
75
+ orgId: input.orgId,
76
+ });
77
+ assertRemoteHostCanRun(device, input.meta);
78
+ const latest = await resourceGetByPath(input.resource.owner, input.resource.path);
79
+ if (!latest || latest.id !== input.resource.id) {
80
+ throw new Error("The automation changed before remote dispatch.");
81
+ }
82
+ const parsed = parseJobResource(latest.content);
83
+ if (parsed.meta.lastStatus !== "running") {
84
+ throw new Error("The automation is no longer marked as running.");
85
+ }
86
+ const requestId = parsed.meta.remoteRequestId ||
87
+ input.meta.remoteRequestId ||
88
+ remoteRequestId(latest, parsed.meta, now);
89
+ let currentResource = latest;
90
+ let currentMeta = parsed.meta;
91
+ if (!currentMeta.remoteRequestId) {
92
+ const withRequest = { ...currentMeta, remoteRequestId: requestId };
93
+ const claimed = await resourcePutIfCurrent({
94
+ owner: latest.owner,
95
+ path: latest.path,
96
+ content: buildJobResourceContent(withRequest, parsed.body),
97
+ expectedId: latest.id,
98
+ expectedUpdatedAt: latest.updatedAt,
99
+ expectedContent: latest.content,
100
+ });
101
+ if (!claimed)
102
+ throw new Error("The automation changed before remote dispatch.");
103
+ currentResource = claimed;
104
+ currentMeta = withRequest;
105
+ }
106
+ let automationRunId = input.historyId || currentMeta.remoteAutomationRunId;
107
+ let createdHistory = false;
108
+ if (!automationRunId) {
109
+ try {
110
+ const historyOwner = input.orgId
111
+ ? organizationResourceOwner(input.orgId)
112
+ : input.resource.owner === "__shared__"
113
+ ? input.ownerEmail
114
+ : input.resource.owner;
115
+ automationRunId = await startAutomationRun({
116
+ owner: historyOwner,
117
+ automation: automationName(input.resource.path),
118
+ path: input.resource.path,
119
+ scope: input.orgId ? "organization" : "personal",
120
+ orgId: input.orgId ?? null,
121
+ appId: input.appId,
122
+ });
123
+ createdHistory = true;
124
+ }
125
+ catch (error) {
126
+ console.warn(`[automations] Could not open remote history for "${input.resource.path}"; queueing without it:`, error);
127
+ automationRunId = undefined;
128
+ }
129
+ }
130
+ try {
131
+ const command = await enqueueRemoteCommand({
132
+ deviceId: device.id,
133
+ ownerEmail: input.ownerEmail,
134
+ orgId: input.orgId ?? null,
135
+ kind: "create-run",
136
+ idempotencyKey: requestId,
137
+ platform: "automation",
138
+ externalThreadId: `automation:${automationName(input.resource.path)}`,
139
+ params: {
140
+ prompt: input.prompt,
141
+ title: input.title,
142
+ cwd: currentMeta.executionCwd,
143
+ goalId: `automation:${automationName(input.resource.path)}`,
144
+ permissionMode: "full-auto",
145
+ engine: currentMeta.executionEngine,
146
+ model: currentMeta.model,
147
+ workload: "scheduled-code",
148
+ metadata: {
149
+ source: "scheduled-automation",
150
+ automation: {
151
+ name: automationName(input.resource.path),
152
+ path: input.resource.path,
153
+ runAt: currentMeta.lastRun,
154
+ },
155
+ ...(automationRunId ? { automationRunId } : {}),
156
+ },
157
+ },
158
+ });
159
+ const nextMeta = {
160
+ ...currentMeta,
161
+ remoteRequestId: requestId,
162
+ remoteCommandId: command.id,
163
+ remoteRunId: remoteRunIdFromCommand(command),
164
+ remoteAutomationRunId: automationRunId,
165
+ remoteAdvanceSchedule: input.advanceSchedule !== false,
166
+ };
167
+ const persisted = await resourcePutIfCurrent({
168
+ owner: currentResource.owner,
169
+ path: currentResource.path,
170
+ content: buildJobResourceContent(nextMeta, parsed.body),
171
+ expectedId: currentResource.id,
172
+ expectedUpdatedAt: currentResource.updatedAt,
173
+ expectedContent: currentResource.content,
174
+ });
175
+ if (!persisted) {
176
+ console.warn(`[automations] Remote command ${command.id} was queued but the automation bookkeeping changed concurrently.`);
177
+ }
178
+ return { command, requestId, automationRunId };
179
+ }
180
+ catch (error) {
181
+ if (createdHistory && automationRunId) {
182
+ await finishAutomationRun(automationRunId, "error", error instanceof Error ? error.message : "Remote dispatch failed.").catch(() => undefined);
183
+ }
184
+ throw error;
185
+ }
186
+ }
187
+ export async function getRemoteAutomationStatus(input) {
188
+ if (!input.meta.executionHostId)
189
+ return { state: "not-remote" };
190
+ if (!input.meta.remoteRequestId) {
191
+ return {
192
+ state: "failed",
193
+ error: "Remote dispatch bookkeeping is missing its request id.",
194
+ };
195
+ }
196
+ const device = await resolveRemoteHost({
197
+ hostId: input.meta.executionHostId,
198
+ ownerEmail: input.ownerEmail,
199
+ orgId: input.orgId,
200
+ });
201
+ if (device.status !== "active") {
202
+ return {
203
+ state: "failed",
204
+ error: `Execution host "${device.label}" is revoked or inactive.`,
205
+ };
206
+ }
207
+ const command = input.meta.remoteCommandId
208
+ ? await getRemoteCommand(input.meta.remoteCommandId)
209
+ : await getRemoteCommandByIdempotencyKey({
210
+ deviceId: device.id,
211
+ idempotencyKey: input.meta.remoteRequestId,
212
+ });
213
+ if (command &&
214
+ (command.deviceId !== device.id ||
215
+ command.ownerEmail !== input.ownerEmail ||
216
+ command.orgId !== (input.orgId ?? null))) {
217
+ return {
218
+ state: "failed",
219
+ error: "Remote dispatch bookkeeping failed its host ownership check.",
220
+ };
221
+ }
222
+ if (!command) {
223
+ const startedAt = input.meta.lastRun
224
+ ? Date.parse(input.meta.lastRun)
225
+ : Number.NaN;
226
+ const age = (input.now ?? new Date()).getTime() - startedAt;
227
+ return age >= REMOTE_AUTOMATION_MAX_ACTIVE_MS
228
+ ? {
229
+ state: "failed",
230
+ error: "Remote dispatch did not produce a command within 24 hours.",
231
+ }
232
+ : { state: "active" };
233
+ }
234
+ if (command.status === "failed") {
235
+ return {
236
+ state: "failed",
237
+ command,
238
+ error: command.errorMessage ?? "The remote command failed.",
239
+ };
240
+ }
241
+ if (command.status !== "completed") {
242
+ const activeAt = command.updatedAt || command.claimedAt || command.createdAt;
243
+ const age = (input.now ?? new Date()).getTime() - activeAt;
244
+ if (Number.isFinite(age) && age >= REMOTE_AUTOMATION_MAX_ACTIVE_MS) {
245
+ return {
246
+ state: "failed",
247
+ command,
248
+ error: "The remote command remained active for more than 24 hours.",
249
+ };
250
+ }
251
+ return { state: "active", command };
252
+ }
253
+ const result = isRecord(command.result) ? command.result : undefined;
254
+ const nested = isRecord(result?.result) ? result.result : result;
255
+ if (nested?.ok === false) {
256
+ return {
257
+ state: "failed",
258
+ command,
259
+ error: command.errorMessage ??
260
+ readString(nested.error) ??
261
+ "Remote run failed.",
262
+ };
263
+ }
264
+ const run = isRecord(nested?.run) ? nested.run : undefined;
265
+ const runStatus = readString(run?.status);
266
+ if (runStatus === "completed")
267
+ return { state: "completed", command };
268
+ if (runStatus === "errored" || runStatus === "paused") {
269
+ return {
270
+ state: "failed",
271
+ command,
272
+ error: readString(run?.error, run?.errorMessage) ??
273
+ command.errorMessage ??
274
+ `Remote run ended with status ${runStatus}.`,
275
+ };
276
+ }
277
+ if (runStatus === "needs-approval")
278
+ return { state: "active", command };
279
+ // A future container/Kubernetes adapter may complete a command without
280
+ // exposing the desktop run shape. Its terminal command is still authoritative.
281
+ return { state: "completed", command };
282
+ }
283
+ export async function finishRemoteAutomationHistory(meta, state, error) {
284
+ if (!meta.remoteAutomationRunId)
285
+ return;
286
+ await finishAutomationRun(meta.remoteAutomationRunId, state === "completed" ? "success" : "error", error);
287
+ }
@@ -2,6 +2,7 @@ import { resourceGetByPath, resourceListAllOwners, resourcePutIfCurrent, } from
2
2
  import { backgroundRunCutOffReason, isBackgroundAutomationRunActive, resolveBackgroundAutomationIdentity, runBackgroundAutomation, } from "./background-automation-runner.js";
3
3
  import { nextOccurrence, isValidCron, describeCron, effectiveTimezone, } from "./cron.js";
4
4
  import { buildJobResourceContent, jobBelongsToApp, parseJobResource, } from "./frontmatter.js";
5
+ import { dispatchRemoteAutomation, finishRemoteAutomationHistory, getRemoteAutomationStatus, } from "./remote-execution.js";
5
6
  import { claimAutomationRun, finishAutomationRun, getAutomationRun, listAutomationRuns, } from "./run-history.js";
6
7
  import { acquireAutomationSchedulerLease, recordAutomationSchedulerHealth, releaseAutomationSchedulerLease, renewAutomationSchedulerLease, AUTOMATION_SCHEDULER_LEASE_RENEWAL_MS, } from "./scheduler-health.js";
7
8
  // ─── Frontmatter parsing ────────────────────────────────────────────────────
@@ -150,6 +151,12 @@ async function processRecurringJobsWithLease(deps) {
150
151
  if (!jobBelongsToApp(meta, deps.appId))
151
152
  continue;
152
153
  healthOrgIds.add(meta.orgId ?? null);
154
+ // A host-targeted run is reconciled from the durable relay command. It
155
+ // must never fall back to this scheduler after the laptop disconnects.
156
+ if (meta.lastStatus === "running" && meta.executionHostId) {
157
+ await reconcileRemoteJob(resource, meta, now);
158
+ continue;
159
+ }
153
160
  // Skip disabled or missing schedule
154
161
  if (!meta.enabled || !meta.schedule)
155
162
  continue;
@@ -341,6 +348,56 @@ function hasRecentIdentityFailure(meta, now) {
341
348
  elapsedMs >= 0 &&
342
349
  elapsedMs < IDENTITY_FAILURE_RETRY_MS);
343
350
  }
351
+ async function reconcileRemoteJob(resource, meta, now) {
352
+ const ownerEmail = meta.createdBy?.trim() || resource.owner;
353
+ try {
354
+ const remote = await getRemoteAutomationStatus({
355
+ meta,
356
+ ownerEmail,
357
+ orgId: meta.orgId,
358
+ now,
359
+ });
360
+ if (remote.state === "active")
361
+ return;
362
+ const error = remote.error ??
363
+ (remote.state === "failed"
364
+ ? "The remote execution host did not complete this automation."
365
+ : undefined);
366
+ await finishRemoteAutomationHistory(meta, remote.state === "completed" ? "completed" : "failed", error).catch((historyError) => {
367
+ console.warn(`[recurring-jobs] Could not finish remote history for "${resource.path}":`, historyError);
368
+ });
369
+ await recordExecutionOutcome(resource, {
370
+ lastRun: meta.lastRun,
371
+ lastStatus: remote.state === "completed" ? "success" : "error",
372
+ lastError: error,
373
+ remoteRequestId: undefined,
374
+ remoteCommandId: undefined,
375
+ remoteRunId: undefined,
376
+ remoteAutomationRunId: undefined,
377
+ remoteAdvanceSchedule: undefined,
378
+ advanceSchedule: meta.remoteAdvanceSchedule !== false,
379
+ });
380
+ console.log(`[recurring-jobs] Remote job "${resource.path}" reached ${remote.state}.`);
381
+ }
382
+ catch (error) {
383
+ const message = error instanceof Error
384
+ ? error.message.slice(0, 200)
385
+ : "Remote execution host could not be reached.";
386
+ await finishRemoteAutomationHistory(meta, "failed", message).catch(() => undefined);
387
+ await recordExecutionOutcome(resource, {
388
+ lastRun: meta.lastRun,
389
+ lastStatus: "error",
390
+ lastError: message,
391
+ remoteRequestId: undefined,
392
+ remoteCommandId: undefined,
393
+ remoteRunId: undefined,
394
+ remoteAutomationRunId: undefined,
395
+ remoteAdvanceSchedule: undefined,
396
+ advanceSchedule: meta.remoteAdvanceSchedule !== false,
397
+ });
398
+ console.error(`[recurring-jobs] Remote job "${resource.path}" failed:`, message);
399
+ }
400
+ }
344
401
  async function executeJob(resource, meta, body, deps, now, options = {}) {
345
402
  const jobName = resource.path.replace(/^jobs\//, "").replace(/\.md$/, "");
346
403
  const jobContext = {
@@ -384,6 +441,50 @@ async function executeJob(resource, meta, body, deps, now, options = {}) {
384
441
  error: "The automation changed before the run could start.",
385
442
  };
386
443
  }
444
+ const prompt = options.manual
445
+ ? `[Manual Automation Run: ${jobName}]\nThis run was explicitly started by the automation owner. Execute the following instructions now:\n\n${body}`
446
+ : `[Recurring Job: ${jobName}]\nSchedule: ${describeCron(meta.schedule, effectiveTimezone(meta.timezone))}\n\nExecute the following job instructions:\n\n${body}`;
447
+ if (meta.executionHostId) {
448
+ try {
449
+ const dispatch = await dispatchRemoteAutomation({
450
+ resource,
451
+ meta,
452
+ body,
453
+ ownerEmail: jobUserEmail,
454
+ orgId: jobOrgId,
455
+ appId: deps.appId,
456
+ prompt,
457
+ title: `${options.manual ? "Automation" : "Job"}: ${jobName}`,
458
+ historyId: options.historyId,
459
+ advanceSchedule: options.advanceSchedule,
460
+ now,
461
+ });
462
+ console.log(`[recurring-jobs] Job "${jobName}" queued on remote host as ${dispatch.command.id}.`);
463
+ return { status: "success", runId: dispatch.command.id };
464
+ }
465
+ catch (err) {
466
+ const lastError = err instanceof Error
467
+ ? err.message.slice(0, 200)
468
+ : "Remote dispatch failed";
469
+ const reportedError = `${lastError}. No delivery was confirmed.`;
470
+ await recordExecutionOutcome(resource, {
471
+ lastRun: meta.lastRun,
472
+ lastStatus: "error",
473
+ lastError: reportedError,
474
+ remoteRequestId: undefined,
475
+ remoteCommandId: undefined,
476
+ remoteRunId: undefined,
477
+ remoteAutomationRunId: undefined,
478
+ remoteAdvanceSchedule: undefined,
479
+ advanceSchedule: options.advanceSchedule,
480
+ });
481
+ if (options.historyId) {
482
+ await finishAutomationRun(options.historyId, "error", reportedError);
483
+ }
484
+ console.error(`[recurring-jobs] Job "${jobName}" remote dispatch failed:`, reportedError);
485
+ return { status: "error", error: reportedError };
486
+ }
487
+ }
387
488
  const requestContext = meta.originScopeId && meta.deliveryPlatform && meta.deliveryDestination
388
489
  ? {
389
490
  isIntegrationCaller: true,
@@ -413,9 +514,7 @@ async function executeJob(resource, meta, body, deps, now, options = {}) {
413
514
  automation: jobContext,
414
515
  ownerEmail: jobUserEmail,
415
516
  orgId: jobOrgId,
416
- prompt: options.manual
417
- ? `[Manual Automation Run: ${jobName}]\nThis run was explicitly started by the automation owner. Execute the following instructions now:\n\n${body}`
418
- : `[Recurring Job: ${jobName}]\nSchedule: ${describeCron(meta.schedule, effectiveTimezone(meta.timezone))}\n\nExecute the following job instructions:\n\n${body}`,
517
+ prompt,
419
518
  threadTitle: `${options.manual ? "Automation" : "Job"}: ${jobName} — ${now.toLocaleDateString()}`,
420
519
  runIdPrefix: `${options.manual ? "manual" : "job"}-${jobName}`,
421
520
  usageLabel: `${options.manual ? "manual-automation" : "recurring-job"}:${jobName}`,
@@ -75,7 +75,7 @@ export async function authorizeJobMutation(resourceOwner, meta, appId) {
75
75
  return "Only the job's creator (or an org admin) can update or delete it.";
76
76
  }
77
77
  async function runCreate(args, appId) {
78
- const { name, instructions, scope, runAs, model } = args;
78
+ const { name, instructions, scope, runAs, model, executionHostId, executionEngine, executionCwd, } = args;
79
79
  const schedule = typeof args.schedule === "string" && args.schedule.trim()
80
80
  ? args.schedule.trim()
81
81
  : DEFAULT_AUTOMATION_SCHEDULE;
@@ -136,6 +136,15 @@ async function runCreate(args, appId) {
136
136
  ...(typeof model === "string" && model.trim()
137
137
  ? { model: model.trim() }
138
138
  : {}),
139
+ ...(typeof executionHostId === "string" && executionHostId.trim()
140
+ ? { executionHostId: executionHostId.trim() }
141
+ : {}),
142
+ ...(typeof executionEngine === "string" && executionEngine.trim()
143
+ ? { executionEngine: executionEngine.trim() }
144
+ : {}),
145
+ ...(typeof executionCwd === "string" && executionCwd.trim()
146
+ ? { executionCwd: executionCwd.trim() }
147
+ : {}),
139
148
  ...(mcpTools?.length ? { mcpTools } : {}),
140
149
  };
141
150
  const content = buildJobContent(meta, instructions);
@@ -193,6 +202,9 @@ async function runList(args, appId) {
193
202
  deliveryPlatform: meta.deliveryPlatform || null,
194
203
  deliveryDestination: meta.deliveryDestination || null,
195
204
  model: meta.model || null,
205
+ executionHostId: meta.executionHostId || null,
206
+ executionEngine: meta.executionEngine || null,
207
+ executionCwd: meta.executionCwd || null,
196
208
  mcpTools: meta.mcpTools || [],
197
209
  };
198
210
  }));
@@ -203,7 +215,7 @@ async function runList(args, appId) {
203
215
  return JSON.stringify(scheduledJobs, null, 2);
204
216
  }
205
217
  async function runUpdate(args, appId) {
206
- const { name, schedule, instructions, enabled, scope, runAs, model } = args;
218
+ const { name, schedule, instructions, enabled, scope, runAs, model, executionHostId, executionEngine, executionCwd, } = args;
207
219
  const path = `jobs/${name}.md`;
208
220
  // Try to find the resource
209
221
  let resource = await resourceGetByPath(getSharedOwner(), path);
@@ -260,6 +272,24 @@ async function runUpdate(args, appId) {
260
272
  }
261
273
  if (typeof model === "string" && model.trim())
262
274
  meta.model = model.trim();
275
+ if (executionHostId !== undefined) {
276
+ meta.executionHostId =
277
+ typeof executionHostId === "string" && executionHostId.trim()
278
+ ? executionHostId.trim()
279
+ : undefined;
280
+ }
281
+ if (executionEngine !== undefined) {
282
+ meta.executionEngine =
283
+ typeof executionEngine === "string" && executionEngine.trim()
284
+ ? executionEngine.trim()
285
+ : undefined;
286
+ }
287
+ if (executionCwd !== undefined) {
288
+ meta.executionCwd =
289
+ typeof executionCwd === "string" && executionCwd.trim()
290
+ ? executionCwd.trim()
291
+ : undefined;
292
+ }
263
293
  if (args.mcpTools !== undefined) {
264
294
  try {
265
295
  const mcpTools = normalizeJobMcpTools(args.mcpTools) ?? [];
@@ -284,6 +314,9 @@ async function runUpdate(args, appId) {
284
314
  enabled: meta.enabled,
285
315
  nextRun: meta.nextRun,
286
316
  mcpTools: meta.mcpTools || [],
317
+ executionHostId: meta.executionHostId || null,
318
+ executionEngine: meta.executionEngine || null,
319
+ executionCwd: meta.executionCwd || null,
287
320
  });
288
321
  }
289
322
  async function runDelete(args, appId) {
@@ -326,7 +359,9 @@ Actions:
326
359
 
327
360
  Cron format is 5 fields: minute hour day-of-month month day-of-week. Common patterns: '0 9 * * *' (daily 9am), '0 9 * * 1-5' (weekdays 9am), '0 * * * *' (every hour), '0 9 * * 1' (Mondays 9am), '*/30 * * * *' (every 30 min).
328
361
 
329
- For jobs that use a connected MCP, pass the exact tool names in mcpTools. This binds only those tools to the background run; OAuth credentials remain in the connector and are resolved for the job's user/org context.`,
362
+ For jobs that use a connected MCP, pass the exact tool names in mcpTools. This binds only those tools to the background run; OAuth credentials remain in the connector and are resolved for the job's user/org context.
363
+
364
+ To run code-agent work on a paired always-on computer, pass executionHostId (from manage-automations action=list-hosts), and optionally executionEngine and executionCwd. The selected host is explicit and never silently replaced.`,
330
365
  parameters: {
331
366
  type: "object",
332
367
  properties: {
@@ -370,6 +405,18 @@ For jobs that use a connected MCP, pass the exact tool names in mcpTools. This b
370
405
  type: "string",
371
406
  description: "Optional model id for this routine. The channel/app/engine default is used when omitted.",
372
407
  },
408
+ executionHostId: {
409
+ type: "string",
410
+ description: "Optional exact paired execution host id. Use manage-automations action=list-hosts first.",
411
+ },
412
+ executionEngine: {
413
+ type: "string",
414
+ description: "Optional host engine id, for example codex-cli or claude-cli.",
415
+ },
416
+ executionCwd: {
417
+ type: "string",
418
+ description: "Optional workspace path on the selected host; otherwise the connector's configured workspace is used.",
419
+ },
373
420
  mcpTools: {
374
421
  type: "array",
375
422
  items: { type: "string" },
@@ -16,18 +16,18 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
- count?: undefined;
20
19
  updated: number;
21
20
  error?: undefined;
22
21
  ok?: undefined;
23
- } | {
24
22
  count?: undefined;
23
+ } | {
25
24
  updated?: undefined;
26
25
  error: string;
27
26
  ok?: undefined;
28
- } | {
29
27
  count?: undefined;
28
+ } | {
30
29
  updated?: undefined;
31
- ok: boolean;
32
30
  error?: undefined;
31
+ ok: boolean;
32
+ count?: undefined;
33
33
  }>>;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
- error?: undefined;
45
- ok?: undefined;
46
44
  summary: import("./types.js").TraceSummary;
47
45
  spans: import("./types.js").TraceSpan[];
48
46
  id?: undefined;
49
- } | {
50
47
  error?: undefined;
51
48
  ok?: undefined;
49
+ } | {
52
50
  summary?: undefined;
53
51
  spans?: undefined;
54
52
  id: string;
55
- } | {
53
+ error?: undefined;
56
54
  ok?: undefined;
55
+ } | {
57
56
  summary?: undefined;
58
57
  spans?: undefined;
59
58
  id?: undefined;
60
59
  error: any;
60
+ ok?: undefined;
61
61
  } | {
62
- error?: undefined;
63
62
  summary?: undefined;
64
63
  spans?: undefined;
65
64
  id?: undefined;
65
+ error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -75,9 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
+ found?: undefined;
78
79
  deleted?: undefined;
79
- id?: undefined;
80
- provider?: undefined;
81
80
  providers: {
82
81
  id: string;
83
82
  label: string;
@@ -88,46 +87,47 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
88
87
  updatedAt: number;
89
88
  }[];
90
89
  count: number;
91
- found?: undefined;
90
+ provider?: undefined;
92
91
  registered?: undefined;
92
+ id?: undefined;
93
93
  label?: undefined;
94
94
  message?: undefined;
95
95
  } | {
96
96
  deleted?: undefined;
97
- id?: undefined;
98
97
  providers?: undefined;
99
98
  count?: undefined;
100
99
  found: boolean;
101
100
  provider: import("../custom-registry.js").CustomProviderConfig;
102
101
  registered?: undefined;
102
+ id?: undefined;
103
103
  label?: undefined;
104
104
  message?: undefined;
105
105
  } | {
106
106
  deleted?: undefined;
107
- provider?: undefined;
108
107
  providers?: undefined;
109
108
  count?: undefined;
109
+ provider?: undefined;
110
110
  found: boolean;
111
111
  id: string;
112
112
  registered?: undefined;
113
113
  label?: undefined;
114
114
  message?: undefined;
115
115
  } | {
116
- provider?: undefined;
116
+ found?: undefined;
117
117
  providers?: undefined;
118
118
  count?: undefined;
119
- found?: undefined;
119
+ provider?: undefined;
120
120
  deleted: boolean;
121
121
  id: string;
122
122
  registered?: undefined;
123
123
  label?: undefined;
124
124
  message?: undefined;
125
125
  } | {
126
+ found?: undefined;
126
127
  deleted?: undefined;
127
- provider?: undefined;
128
128
  providers?: undefined;
129
129
  count?: undefined;
130
- found?: undefined;
130
+ provider?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
133
133
  label: string;