@agent-native/core 0.157.8 → 0.157.10

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 (154) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +26 -8
  3. package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +261 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
  5. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +75 -31
  6. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +4 -1
  7. package/corpus/templates/analytics/server/plugins/db.ts +139 -0
  8. package/corpus/templates/slides/AGENTS.md +14 -10
  9. package/corpus/templates/slides/actions/export-pptx.ts +1311 -121
  10. package/corpus/templates/slides/actions/import-file.ts +28 -1
  11. package/corpus/templates/slides/actions/import-pptx.ts +69 -42
  12. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
  13. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
  14. package/corpus/templates/slides/app/global.css +19 -1
  15. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
  16. package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
  17. package/corpus/templates/slides/app/lib/export-pptx-client.ts +601 -30
  18. package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
  19. package/corpus/templates/slides/server/handlers/import/html-converter.ts +1240 -22
  20. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +329 -64
  21. package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
  22. package/corpus/templates/slides/server/lib/source-import.ts +10 -1
  23. package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
  24. package/dist/agent/harness/migrations.d.ts +4 -0
  25. package/dist/agent/harness/migrations.js +60 -0
  26. package/dist/agent/production-agent.d.ts +2 -0
  27. package/dist/agent/production-agent.js +15 -0
  28. package/dist/agent/run-loop-with-resume.d.ts +1 -0
  29. package/dist/agent/run-loop-with-resume.js +44 -12
  30. package/dist/agent/run-migrations.d.ts +8 -0
  31. package/dist/agent/run-migrations.js +61 -0
  32. package/dist/automations/service.d.ts +6 -0
  33. package/dist/automations/service.js +35 -0
  34. package/dist/chat-threads/migrations.d.ts +1 -0
  35. package/dist/chat-threads/migrations.js +1 -0
  36. package/dist/chat-threads/schema-migrations.d.ts +8 -0
  37. package/dist/chat-threads/schema-migrations.js +60 -0
  38. package/dist/cli/code-agent-connector.d.ts +4 -1
  39. package/dist/cli/code-agent-connector.js +154 -23
  40. package/dist/cli/code-agent-executor.js +255 -13
  41. package/dist/cli/code-agent-runs.d.ts +1 -0
  42. package/dist/cli/code-agent-runs.js +2 -1
  43. package/dist/cli/portal-workspace.d.ts +49 -0
  44. package/dist/cli/portal-workspace.js +320 -0
  45. package/dist/client/AssistantChat.d.ts +1 -1
  46. package/dist/client/AssistantChat.js +43 -61
  47. package/dist/client/ConnectBuilderCard.js +33 -5
  48. package/dist/client/agent-chat-adapter.js +4 -0
  49. package/dist/client/analytics-platform.d.ts +2 -0
  50. package/dist/client/analytics-platform.js +12 -0
  51. package/dist/client/analytics.d.ts +3 -0
  52. package/dist/client/analytics.js +10 -1
  53. package/dist/client/chat/index.d.ts +1 -0
  54. package/dist/client/chat/index.js +1 -0
  55. package/dist/client/chat/message-components.d.ts +2 -0
  56. package/dist/client/chat/message-components.js +11 -1
  57. package/dist/client/chat/run-recovery.d.ts +2 -1
  58. package/dist/client/chat/run-recovery.js +2 -2
  59. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  60. package/dist/client/chat-first/apps-rail.js +11 -11
  61. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  62. package/dist/client/chat-first/surface-tabs.js +38 -36
  63. package/dist/client/chat-first/types.d.ts +5 -0
  64. package/dist/client/chat-first.d.ts +1 -1
  65. package/dist/client/chat-first.js +4 -4
  66. package/dist/client/desktop-local-code-change.d.ts +10 -0
  67. package/dist/client/desktop-local-code-change.js +16 -0
  68. package/dist/client/index.d.ts +1 -0
  69. package/dist/client/index.js +1 -0
  70. package/dist/client/observability/ThumbsFeedback.js +53 -26
  71. package/dist/client/observability/useObservability.d.ts +1 -1
  72. package/dist/client/observability/useObservability.js +6 -3
  73. package/dist/client/terminal/AgentTerminal.d.ts +9 -1
  74. package/dist/client/terminal/AgentTerminal.js +38 -3
  75. package/dist/client/terminal/index.d.ts +1 -1
  76. package/dist/client/terminal/index.js +1 -1
  77. package/dist/client/track.js +9 -1
  78. package/dist/client/use-action.js +3 -0
  79. package/dist/collab/awareness.d.ts +2 -2
  80. package/dist/deploy/build.js +1 -1
  81. package/dist/feature-flags/a2a-action-route.js +4 -2
  82. package/dist/feature-flags/permissions.d.ts +2 -0
  83. package/dist/feature-flags/permissions.js +6 -1
  84. package/dist/ingestion/pptx.d.ts +92 -1
  85. package/dist/ingestion/pptx.js +1180 -115
  86. package/dist/integrations/index.d.ts +3 -3
  87. package/dist/integrations/index.js +2 -2
  88. package/dist/integrations/plugin.js +95 -6
  89. package/dist/integrations/remote-commands-store.d.ts +5 -0
  90. package/dist/integrations/remote-commands-store.js +63 -22
  91. package/dist/integrations/remote-devices-store.d.ts +2 -1
  92. package/dist/integrations/remote-devices-store.js +46 -0
  93. package/dist/integrations/remote-types.d.ts +20 -0
  94. package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
  95. package/dist/jobs/actions/list-recurring-jobs.js +5 -1
  96. package/dist/jobs/frontmatter.d.ts +16 -0
  97. package/dist/jobs/frontmatter.js +53 -0
  98. package/dist/jobs/remote-execution.d.ts +42 -0
  99. package/dist/jobs/remote-execution.js +287 -0
  100. package/dist/jobs/scheduler.js +102 -3
  101. package/dist/jobs/tools.js +50 -3
  102. package/dist/localization/core-messages/ar-SA.js +4 -0
  103. package/dist/localization/core-messages/de-DE.js +4 -0
  104. package/dist/localization/core-messages/en-US.d.ts +4 -0
  105. package/dist/localization/core-messages/en-US.js +4 -0
  106. package/dist/localization/core-messages/es-ES.js +4 -0
  107. package/dist/localization/core-messages/fr-FR.js +4 -0
  108. package/dist/localization/core-messages/hi-IN.js +4 -0
  109. package/dist/localization/core-messages/ja-JP.js +4 -0
  110. package/dist/localization/core-messages/ko-KR.js +4 -0
  111. package/dist/localization/core-messages/pt-BR.js +4 -0
  112. package/dist/localization/core-messages/zh-CN.js +4 -0
  113. package/dist/localization/core-messages/zh-TW.js +4 -0
  114. package/dist/localization/core-messages.d.ts +4 -0
  115. package/dist/notifications/routes.d.ts +3 -3
  116. package/dist/observability/feedback.js +40 -16
  117. package/dist/observability/routes.d.ts +6 -6
  118. package/dist/observability/routes.js +15 -3
  119. package/dist/observability/store.js +2 -0
  120. package/dist/observability/traces.js +5 -1
  121. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  122. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  123. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  124. package/dist/resources/handlers.d.ts +1 -1
  125. package/dist/secrets/routes.d.ts +9 -9
  126. package/dist/server/action-routes.d.ts +7 -5
  127. package/dist/server/action-routes.js +13 -6
  128. package/dist/server/agent-chat-plugin.js +6 -0
  129. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  130. package/dist/server/agent-run-context.d.ts +1 -0
  131. package/dist/server/agent-run-context.js +9 -0
  132. package/dist/server/better-auth-instance.js +4 -0
  133. package/dist/server/core-routes-plugin.js +8 -1
  134. package/dist/server/realtime-voice.js +2 -1
  135. package/dist/server/release-migrations.js +16 -0
  136. package/dist/server/request-context.d.ts +2 -0
  137. package/dist/server/transcribe-voice.d.ts +1 -1
  138. package/dist/shared/analytics-platform.d.ts +6 -0
  139. package/dist/shared/analytics-platform.js +14 -0
  140. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  141. package/dist/shared/mcp-embed-headers.js +1 -1
  142. package/dist/styles/agent-native.css +83 -103
  143. package/dist/terminal/cli-registry.js +5 -0
  144. package/dist/tracking/registry.js +9 -1
  145. package/dist/triggers/actions/list-automations.d.ts +3 -0
  146. package/dist/triggers/actions/list-automations.js +3 -0
  147. package/dist/triggers/actions/manage-automation.d.ts +9 -0
  148. package/dist/triggers/actions/manage-automation.js +14 -2
  149. package/dist/triggers/actions.js +82 -7
  150. package/dist/usage/migrations.d.ts +4 -0
  151. package/dist/usage/migrations.js +45 -0
  152. package/docs/content/template-content-developers.mdx +48 -0
  153. package/docs/content/toolkit-ui.mdx +57 -0
  154. package/package.json +3 -3
@@ -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" },
@@ -273,10 +273,14 @@ const messages = {
273
273
  "errorMessages.startNewChat": "بدء محادثة جديدة",
274
274
  "errorMessages.upgradeAtBuilder": "الترقية عبر Builder.io",
275
275
  "feedback.inaccurate": "غير دقيق",
276
+ "feedback.keyboardHint": "{{shortcut}} Enter للإرسال",
276
277
  "feedback.notHelpful": "غير مفيد",
278
+ "feedback.placeholder": "أخبرنا بما حدث من خطأ...",
279
+ "feedback.submit": "إرسال",
277
280
  "feedback.thumbsDown": "عدم إعجاب",
278
281
  "feedback.thumbsUp": "إعجاب",
279
282
  "feedback.tooSlow": "بطيء جدًا",
283
+ "feedback.whatWentWrong": "ما الذي حدث بشكل خاطئ؟",
280
284
  "feedback.wrongTool": "أداة غير صحيحة",
281
285
  "header.switchToCli": "التبديل إلى CLI",
282
286
  "history.active": "نشطة",
@@ -401,10 +401,14 @@ const messages = {
401
401
  "errorMessages.startNewChat": "Neuen Chat starten",
402
402
  "errorMessages.upgradeAtBuilder": "Bei Builder.io upgraden",
403
403
  "feedback.inaccurate": "Ungenau",
404
+ "feedback.keyboardHint": "{{shortcut}} Enter zum Senden",
404
405
  "feedback.notHelpful": "Nicht hilfreich",
406
+ "feedback.placeholder": "Sag uns, was schiefgelaufen ist...",
407
+ "feedback.submit": "Senden",
405
408
  "feedback.thumbsDown": "Daumen runter",
406
409
  "feedback.thumbsUp": "Daumen hoch",
407
410
  "feedback.tooSlow": "Zu langsam",
411
+ "feedback.whatWentWrong": "Was ist schiefgelaufen?",
408
412
  "feedback.wrongTool": "Falsches Tool",
409
413
  "contextMeter.ariaLabel": "Kontext {{percent}} %, {{totalTokens}}{{breakdown}}. Kontextanalyse öffnen.",
410
414
  "contextMeter.breakdown": " gesamt: {{systemTokens}} System + {{conversationTokens}} Unterhaltung",
@@ -265,10 +265,14 @@ declare const messages: {
265
265
  readonly "errorMessages.startNewChat": "Start new chat";
266
266
  readonly "errorMessages.upgradeAtBuilder": "Upgrade at Builder.io";
267
267
  readonly "feedback.inaccurate": "Inaccurate";
268
+ readonly "feedback.keyboardHint": "{{shortcut}} Enter to send";
268
269
  readonly "feedback.notHelpful": "Not helpful";
270
+ readonly "feedback.placeholder": "Tell us what went wrong...";
271
+ readonly "feedback.submit": "Submit";
269
272
  readonly "feedback.thumbsDown": "Thumbs down";
270
273
  readonly "feedback.thumbsUp": "Thumbs up";
271
274
  readonly "feedback.tooSlow": "Too slow";
275
+ readonly "feedback.whatWentWrong": "What went wrong?";
272
276
  readonly "feedback.wrongTool": "Wrong tool";
273
277
  readonly "header.switchToCli": "Switch to CLI";
274
278
  readonly "history.active": "Active";
@@ -265,10 +265,14 @@ const messages = {
265
265
  "errorMessages.startNewChat": "Start new chat",
266
266
  "errorMessages.upgradeAtBuilder": "Upgrade at Builder.io",
267
267
  "feedback.inaccurate": "Inaccurate",
268
+ "feedback.keyboardHint": "{{shortcut}} Enter to send",
268
269
  "feedback.notHelpful": "Not helpful",
270
+ "feedback.placeholder": "Tell us what went wrong...",
271
+ "feedback.submit": "Submit",
269
272
  "feedback.thumbsDown": "Thumbs down",
270
273
  "feedback.thumbsUp": "Thumbs up",
271
274
  "feedback.tooSlow": "Too slow",
275
+ "feedback.whatWentWrong": "What went wrong?",
272
276
  "feedback.wrongTool": "Wrong tool",
273
277
  "header.switchToCli": "Switch to CLI",
274
278
  "history.active": "Active",
@@ -408,10 +408,14 @@ const messages = {
408
408
  "errorMessages.startNewChat": "Iniciar un chat nuevo",
409
409
  "errorMessages.upgradeAtBuilder": "Mejorar el plan en Builder.io",
410
410
  "feedback.inaccurate": "Impreciso",
411
+ "feedback.keyboardHint": "{{shortcut}} Enter para enviar",
411
412
  "feedback.notHelpful": "Poco útil",
413
+ "feedback.placeholder": "Cuéntanos qué salió mal...",
414
+ "feedback.submit": "Enviar",
412
415
  "feedback.thumbsDown": "No me gusta",
413
416
  "feedback.thumbsUp": "Me gusta",
414
417
  "feedback.tooSlow": "Demasiado lento",
418
+ "feedback.whatWentWrong": "¿Qué salió mal?",
415
419
  "feedback.wrongTool": "Herramienta incorrecta",
416
420
  "contextMeter.ariaLabel": "Contexto {{percent}} %, {{totalTokens}}{{breakdown}}. Abrir el análisis de contexto.",
417
421
  "contextMeter.breakdown": " en total: {{systemTokens}} del sistema + {{conversationTokens}} de la conversación",
@@ -408,10 +408,14 @@ const messages = {
408
408
  "errorMessages.startNewChat": "Démarrer une nouvelle discussion",
409
409
  "errorMessages.upgradeAtBuilder": "Mettre à niveau sur Builder.io",
410
410
  "feedback.inaccurate": "Inexact",
411
+ "feedback.keyboardHint": "{{shortcut}} Entrée pour envoyer",
411
412
  "feedback.notHelpful": "Peu utile",
413
+ "feedback.placeholder": "Dites-nous ce qui n'a pas fonctionné...",
414
+ "feedback.submit": "Envoyer",
412
415
  "feedback.thumbsDown": "Pouce vers le bas",
413
416
  "feedback.thumbsUp": "Pouce vers le haut",
414
417
  "feedback.tooSlow": "Trop lent",
418
+ "feedback.whatWentWrong": "Qu'est-ce qui n'a pas fonctionné ?",
415
419
  "feedback.wrongTool": "Mauvais outil",
416
420
  "contextMeter.ariaLabel": "Contexte {{percent}} %, {{totalTokens}}{{breakdown}}. Ouvrir l’analyse du contexte.",
417
421
  "contextMeter.breakdown": " au total : {{systemTokens}} système + {{conversationTokens}} conversation",
@@ -265,10 +265,14 @@ const messages = {
265
265
  "errorMessages.startNewChat": "नई चैट शुरू करें",
266
266
  "errorMessages.upgradeAtBuilder": "Builder.io पर अपग्रेड करें",
267
267
  "feedback.inaccurate": "गलत जानकारी",
268
+ "feedback.keyboardHint": "भेजने के लिए {{shortcut}} Enter",
268
269
  "feedback.notHelpful": "उपयोगी नहीं",
270
+ "feedback.placeholder": "बताएं कि क्या गलत हुआ...",
271
+ "feedback.submit": "जमा करें",
269
272
  "feedback.thumbsDown": "नापसंद",
270
273
  "feedback.thumbsUp": "पसंद",
271
274
  "feedback.tooSlow": "बहुत धीमा",
275
+ "feedback.whatWentWrong": "क्या गलत हुआ?",
272
276
  "feedback.wrongTool": "गलत टूल",
273
277
  "header.switchToCli": "CLI पर जाएँ",
274
278
  "history.active": "सक्रिय",
@@ -263,10 +263,14 @@ const messages = {
263
263
  "errorMessages.startNewChat": "新しいチャットを開始",
264
264
  "errorMessages.upgradeAtBuilder": "Builder.io でアップグレード",
265
265
  "feedback.inaccurate": "不正確",
266
+ "feedback.keyboardHint": "{{shortcut}} Enter で送信",
266
267
  "feedback.notHelpful": "役に立たない",
268
+ "feedback.placeholder": "問題点を教えてください…",
269
+ "feedback.submit": "送信",
267
270
  "feedback.thumbsDown": "低評価",
268
271
  "feedback.thumbsUp": "高評価",
269
272
  "feedback.tooSlow": "遅すぎる",
273
+ "feedback.whatWentWrong": "何が問題でしたか?",
270
274
  "feedback.wrongTool": "ツールが不適切",
271
275
  "header.switchToCli": "CLI に切り替え",
272
276
  "history.active": "アクティブ",