@agent-native/core-corpus 0.203.1-nightly-20261007183952 → 0.203.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/templates/analytics/CHANGELOG.md +4 -0
- package/corpus/templates/content/CHANGELOG.md +2 -0
- package/corpus/templates/design/CHANGELOG.md +7 -0
- package/corpus/templates/dispatch/.agents/skills/recurring-jobs/SKILL.md +16 -1
- package/corpus/templates/factory/actions/save-factory-automation.ts +17 -5
- package/corpus/templates/factory/server/lib/factory-automation-config.ts +55 -0
- package/corpus/templates/factory/server/lib/factory-automation-history.ts +116 -14
- package/package.json +1 -1
|
@@ -11,6 +11,10 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
11
11
|
- Save exact session replay screens as PNG screenshots.
|
|
12
12
|
- Session replays can be saved as screenshots without the replay controls.
|
|
13
13
|
|
|
14
|
+
### Improved
|
|
15
|
+
|
|
16
|
+
- Agent dashboard edits are now checked against the live chart before they save, so the agent no longer reports a chart change that did not happen.
|
|
17
|
+
|
|
14
18
|
### Fixed
|
|
15
19
|
|
|
16
20
|
- Collaborators see a consistent dashboard after simultaneous edits
|
|
@@ -7,6 +7,8 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
7
7
|
|
|
8
8
|
### Fixed
|
|
9
9
|
|
|
10
|
+
- ChatGPT widget previews show saved document bodies without a Content session.
|
|
11
|
+
- Documents opened in ChatGPT show their saved content right away.
|
|
10
12
|
- ChatGPT widgets show saved document bodies in read-only mode without a Content session.
|
|
11
13
|
- Fixed Content pages that opened to an error in ChatGPT widgets
|
|
12
14
|
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable user-facing changes to Design are documented here. Open it any time
|
|
4
4
|
from the command menu (Cmd+K → "What's new") or from Settings.
|
|
5
5
|
|
|
6
|
+
## 2026-10-07
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Design keeps frame-relative positions and selections stable when moving or pasting layers.
|
|
11
|
+
- Position values stay aligned with selected layers in layouts with body margins.
|
|
12
|
+
|
|
6
13
|
## 2026-10-06
|
|
7
14
|
|
|
8
15
|
### Added
|
|
@@ -46,10 +46,25 @@ and pass normalized action items to an app's idempotent `import` action.
|
|
|
46
46
|
|
|
47
47
|
| Tool | Action | Purpose |
|
|
48
48
|
| ------------- | ---------- | ---------------------------------------------------------- |
|
|
49
|
-
| `manage-jobs` | `create` | Create a recurring job (name, cron schedule, instructions) |
|
|
49
|
+
| `manage-jobs` | `create` | Create a recurring job (name, cron schedule, instructions); fails if the name exists |
|
|
50
50
|
| `manage-jobs` | `list` | List all jobs and their status |
|
|
51
51
|
| `manage-jobs` | `update` | Update schedule, instructions, or toggle enabled |
|
|
52
52
|
|
|
53
|
+
## Job files an app owns
|
|
54
|
+
|
|
55
|
+
`create` never replaces an existing job file, so a name that collides with an
|
|
56
|
+
app's file (Factory's `factories/<id>/<name>`) is refused instead of reset.
|
|
57
|
+
An app that stores its own settings in a job's frontmatter marks the file with
|
|
58
|
+
`triggerType`, which makes `list`, `update`, and `delete` skip it and sends the
|
|
59
|
+
caller to `manage-automations`. Edit those files only through the owning app's
|
|
60
|
+
actions, and patch fields in place; rebuilding a file from the fields the writer
|
|
61
|
+
knows drops the rest while the job keeps running.
|
|
62
|
+
|
|
63
|
+
Any write to a `jobs/` file that removes a frontmatter field other than run
|
|
64
|
+
bookkeeping is recorded as a `job-fields-dropped` audit event (org admins read
|
|
65
|
+
it with `list-audit-events` and `action: "job-fields-dropped"`). Its input names
|
|
66
|
+
the writer, the dropped fields, and the agent thread and run that made it.
|
|
67
|
+
|
|
53
68
|
## UI Surface
|
|
54
69
|
|
|
55
70
|
`/agent#jobs` is the stable compatibility URL for the Agent page's
|
|
@@ -24,11 +24,12 @@ import {
|
|
|
24
24
|
readPromptVersion,
|
|
25
25
|
replaceAutomationContentWithUserPrompt,
|
|
26
26
|
scheduleCron,
|
|
27
|
+
stampAutomationTriggerType,
|
|
27
28
|
} from "../server/lib/factory-automation-config.js";
|
|
28
29
|
import {
|
|
29
30
|
deleteFactoryAutomationVersionRow,
|
|
30
31
|
insertFactoryAutomationVersionIfChanged,
|
|
31
|
-
|
|
32
|
+
resolvePromptVersionAllocation,
|
|
32
33
|
snapshotFromAutomationResource,
|
|
33
34
|
} from "../server/lib/factory-automation-history.js";
|
|
34
35
|
import { findFactoryAutomationDefinition } from "../server/lib/factory-automation-resources.js";
|
|
@@ -249,14 +250,17 @@ export default defineAction({
|
|
|
249
250
|
input.displayName !== undefined
|
|
250
251
|
? input.displayName.trim() || null
|
|
251
252
|
: previousSnapshot.displayName;
|
|
252
|
-
const
|
|
253
|
-
|
|
253
|
+
const versionAllocation = await resolvePromptVersionAllocation({
|
|
254
|
+
automationId: definition.resource.id,
|
|
255
|
+
orgId,
|
|
256
|
+
next: {
|
|
254
257
|
userPrompt: normalizedPrompt,
|
|
255
258
|
displayName: nextDisplayName,
|
|
256
259
|
config,
|
|
257
260
|
},
|
|
258
|
-
previousSnapshot,
|
|
259
|
-
);
|
|
261
|
+
previous: previousSnapshot,
|
|
262
|
+
});
|
|
263
|
+
const resolvedPromptVersion = versionAllocation.promptVersion;
|
|
260
264
|
let content = applyAutomationConfigFrontmatter(resource.content, config);
|
|
261
265
|
content = replaceAutomationContentWithUserPrompt(
|
|
262
266
|
content,
|
|
@@ -289,6 +293,13 @@ export default defineAction({
|
|
|
289
293
|
input.factoryId,
|
|
290
294
|
);
|
|
291
295
|
content = setAutomationFrontmatterField(content, "appId", "factory");
|
|
296
|
+
const stamp = stampAutomationTriggerType(content, { orgId });
|
|
297
|
+
content = stamp.content;
|
|
298
|
+
if (stamp.skipped) {
|
|
299
|
+
console.warn(
|
|
300
|
+
`[save-factory-automation] ${input.name} stays untagged because ${stamp.skipped}.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
292
303
|
if (input.model !== undefined) {
|
|
293
304
|
content = setAutomationFrontmatterField(
|
|
294
305
|
content,
|
|
@@ -328,6 +339,7 @@ export default defineAction({
|
|
|
328
339
|
nextContent: content,
|
|
329
340
|
summary: "Automation save",
|
|
330
341
|
source: "save",
|
|
342
|
+
version: versionAllocation.predecessorVersion ?? undefined,
|
|
331
343
|
});
|
|
332
344
|
let updated: Awaited<ReturnType<typeof resourcePutIfCurrent>> = null;
|
|
333
345
|
let writeError: unknown;
|
|
@@ -490,6 +490,61 @@ export function restoreFactoryAutomationIdentityFields(
|
|
|
490
490
|
return next;
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
export type AutomationTriggerStamp = {
|
|
494
|
+
content: string;
|
|
495
|
+
/** Why the file stays untagged; absent when it is tagged. */
|
|
496
|
+
skipped?: string;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Tags a job file with its trigger type so core treats it as an automation.
|
|
501
|
+
* The tag moves the scheduler from its legacy identity fallback to strict
|
|
502
|
+
* checks (`createdBy`, `runAs: creator`, and an `orgId` matching the owner),
|
|
503
|
+
* so a file is tagged only when it can pass them. Defaults the legacy path
|
|
504
|
+
* already assumes are written down; a creator is never invented.
|
|
505
|
+
*/
|
|
506
|
+
export function stampAutomationTriggerType(
|
|
507
|
+
content: string,
|
|
508
|
+
options: { orgId: string; triggerType?: string; identityFrom?: string },
|
|
509
|
+
): AutomationTriggerStamp {
|
|
510
|
+
if (readFrontmatterValue(content, "triggerType") != null) return { content };
|
|
511
|
+
const read = (key: string) =>
|
|
512
|
+
readFrontmatterValue(content, key) ??
|
|
513
|
+
(options.identityFrom
|
|
514
|
+
? readFrontmatterValue(options.identityFrom, key)
|
|
515
|
+
: undefined);
|
|
516
|
+
const createdBy = read("createdBy");
|
|
517
|
+
if (!createdBy) return { content, skipped: "it has no createdBy" };
|
|
518
|
+
const runAs = read("runAs") ?? "creator";
|
|
519
|
+
if (runAs !== "creator") {
|
|
520
|
+
return { content, skipped: `it runs as "${runAs}"` };
|
|
521
|
+
}
|
|
522
|
+
const orgId = read("orgId") ?? options.orgId;
|
|
523
|
+
if (orgId !== options.orgId) {
|
|
524
|
+
return {
|
|
525
|
+
content,
|
|
526
|
+
skipped: `its orgId is "${orgId}", not "${options.orgId}"`,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
let next = content;
|
|
530
|
+
for (const [key, value] of [
|
|
531
|
+
["createdBy", createdBy],
|
|
532
|
+
["runAs", runAs],
|
|
533
|
+
["orgId", orgId],
|
|
534
|
+
] as const) {
|
|
535
|
+
if (readFrontmatterValue(next, key) == null) {
|
|
536
|
+
next = setAutomationFrontmatterField(next, key, value);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
content: setAutomationFrontmatterField(
|
|
541
|
+
next,
|
|
542
|
+
"triggerType",
|
|
543
|
+
options.triggerType ?? "schedule",
|
|
544
|
+
),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
493
548
|
export function applyAutomationConfigFrontmatter(
|
|
494
549
|
content: string,
|
|
495
550
|
config: FactoryAutomationConfig,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { fail } from "@agent-native/core/action";
|
|
2
|
+
import { isUniqueViolation } from "@agent-native/core/db";
|
|
1
3
|
import { type Resource } from "@agent-native/core/resources";
|
|
2
|
-
import { and, desc, eq, lt } from "drizzle-orm";
|
|
4
|
+
import { and, desc, eq, lt, max } from "drizzle-orm";
|
|
3
5
|
|
|
4
6
|
import { getDb } from "../db/index.js";
|
|
5
7
|
import { factoryAutomationVersions } from "../db/schema.js";
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
readFactoryAutomationConfig,
|
|
14
16
|
readFrontmatterValue,
|
|
15
17
|
readPromptVersion,
|
|
18
|
+
stampAutomationTriggerType,
|
|
16
19
|
} from "./factory-automation-config.js";
|
|
17
20
|
import { findFactoryAutomationByResourceId } from "./factory-automation-resources.js";
|
|
18
21
|
import {
|
|
@@ -74,17 +77,73 @@ export function snapshotContentIdentity(
|
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
async function maxStoredVersion(input: {
|
|
81
|
+
automationId: string;
|
|
82
|
+
orgId: string;
|
|
83
|
+
}): Promise<number | null> {
|
|
84
|
+
const [row] = await getDb()
|
|
85
|
+
.select({ latest: max(factoryAutomationVersions.version) })
|
|
86
|
+
.from(factoryAutomationVersions)
|
|
87
|
+
.where(
|
|
88
|
+
and(
|
|
89
|
+
eq(factoryAutomationVersions.automationId, input.automationId),
|
|
90
|
+
eq(factoryAutomationVersions.orgId, input.orgId),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
return row?.latest ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The version number to store the previous content under. A file's own
|
|
98
|
+
* `promptVersion` is the right number until it falls behind its history: a
|
|
99
|
+
* file that lost its frontmatter reads as version 0, and storing that under a
|
|
100
|
+
* number the history already holds fails the whole save.
|
|
101
|
+
*/
|
|
102
|
+
export async function allocatePredecessorVersion(input: {
|
|
103
|
+
automationId: string;
|
|
104
|
+
orgId: string;
|
|
105
|
+
fileVersion: number;
|
|
106
|
+
}): Promise<number> {
|
|
107
|
+
const stored = await maxStoredVersion(input);
|
|
108
|
+
const nextFree = stored === null ? 0 : stored + 1;
|
|
109
|
+
if (input.fileVersion >= nextFree) return input.fileVersion;
|
|
110
|
+
console.warn(
|
|
111
|
+
`[factory-automation-history] ${input.automationId} reports promptVersion ${input.fileVersion} but versions up to ${stored} are already stored; recording its previous content as version ${nextFree}.`,
|
|
112
|
+
);
|
|
113
|
+
return nextFree;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type PromptVersionAllocation = {
|
|
117
|
+
/** The `promptVersion` the saved file carries. */
|
|
118
|
+
promptVersion: number;
|
|
119
|
+
/** Where the previous content is stored; null when nothing changed. */
|
|
120
|
+
predecessorVersion: number | null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export async function resolvePromptVersionAllocation(input: {
|
|
124
|
+
automationId: string;
|
|
125
|
+
orgId: string;
|
|
78
126
|
next: Pick<
|
|
79
127
|
FactoryAutomationSnapshot,
|
|
80
128
|
"userPrompt" | "displayName" | "config"
|
|
81
|
-
|
|
82
|
-
previous: FactoryAutomationSnapshot
|
|
83
|
-
):
|
|
84
|
-
if (
|
|
85
|
-
|
|
129
|
+
>;
|
|
130
|
+
previous: FactoryAutomationSnapshot;
|
|
131
|
+
}): Promise<PromptVersionAllocation> {
|
|
132
|
+
if (
|
|
133
|
+
snapshotContentIdentity(input.previous) ===
|
|
134
|
+
snapshotContentIdentity(input.next)
|
|
135
|
+
) {
|
|
136
|
+
return {
|
|
137
|
+
promptVersion: input.previous.promptVersion,
|
|
138
|
+
predecessorVersion: null,
|
|
139
|
+
};
|
|
86
140
|
}
|
|
87
|
-
|
|
141
|
+
const predecessorVersion = await allocatePredecessorVersion({
|
|
142
|
+
automationId: input.automationId,
|
|
143
|
+
orgId: input.orgId,
|
|
144
|
+
fileVersion: input.previous.promptVersion,
|
|
145
|
+
});
|
|
146
|
+
return { promptVersion: predecessorVersion + 1, predecessorVersion };
|
|
88
147
|
}
|
|
89
148
|
|
|
90
149
|
export type FactoryAutomationVersionRow = {
|
|
@@ -115,17 +174,26 @@ export async function insertFactoryAutomationVersionRow(input: {
|
|
|
115
174
|
content: string;
|
|
116
175
|
summary: string;
|
|
117
176
|
source: FactoryAutomationVersionSource;
|
|
177
|
+
/** Pre-allocated by `resolvePromptVersionAllocation`; allocated here when omitted. */
|
|
178
|
+
version?: number;
|
|
118
179
|
}): Promise<FactoryAutomationVersionRow> {
|
|
119
180
|
const snapshot = snapshotFromAutomationResource(
|
|
120
181
|
input.content,
|
|
121
182
|
input.automationName,
|
|
122
183
|
input.factoryId,
|
|
123
184
|
);
|
|
185
|
+
const version =
|
|
186
|
+
input.version ??
|
|
187
|
+
(await allocatePredecessorVersion({
|
|
188
|
+
automationId: input.automationId,
|
|
189
|
+
orgId: input.orgId,
|
|
190
|
+
fileVersion: snapshot.promptVersion,
|
|
191
|
+
}));
|
|
124
192
|
const row: FactoryAutomationVersionRow = {
|
|
125
193
|
id: createVersionId(),
|
|
126
194
|
automationId: input.automationId,
|
|
127
195
|
factoryId: input.factoryId,
|
|
128
|
-
version
|
|
196
|
+
version,
|
|
129
197
|
rawContent: input.content,
|
|
130
198
|
displayName: snapshot.displayName,
|
|
131
199
|
source: input.source,
|
|
@@ -135,7 +203,21 @@ export async function insertFactoryAutomationVersionRow(input: {
|
|
|
135
203
|
ownerEmail: input.userEmail,
|
|
136
204
|
orgId: input.orgId,
|
|
137
205
|
};
|
|
138
|
-
|
|
206
|
+
try {
|
|
207
|
+
await getDb().insert(factoryAutomationVersions).values(row);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// Drizzle wraps the driver error, so the constraint code is on `cause`.
|
|
210
|
+
if (
|
|
211
|
+
isUniqueViolation(error) ||
|
|
212
|
+
isUniqueViolation((error as { cause?: unknown }).cause)
|
|
213
|
+
) {
|
|
214
|
+
fail(
|
|
215
|
+
"Another change to this automation was saved at the same time. Refresh and try again.",
|
|
216
|
+
{ statusCode: 409, errorCode: "automation_version_conflict" },
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
139
221
|
return row;
|
|
140
222
|
}
|
|
141
223
|
|
|
@@ -149,6 +231,7 @@ export async function insertFactoryAutomationVersionIfChanged(input: {
|
|
|
149
231
|
nextContent: string;
|
|
150
232
|
summary: string;
|
|
151
233
|
source: FactoryAutomationVersionSource;
|
|
234
|
+
version?: number;
|
|
152
235
|
}): Promise<FactoryAutomationVersionRow | null> {
|
|
153
236
|
const previousSnapshot = snapshotFromAutomationResource(
|
|
154
237
|
input.previousContent,
|
|
@@ -175,6 +258,7 @@ export async function insertFactoryAutomationVersionIfChanged(input: {
|
|
|
175
258
|
content: input.previousContent,
|
|
176
259
|
summary: input.summary,
|
|
177
260
|
source: input.source,
|
|
261
|
+
version: input.version,
|
|
178
262
|
});
|
|
179
263
|
}
|
|
180
264
|
|
|
@@ -305,10 +389,13 @@ export async function restoreFactoryAutomationVersion(input: {
|
|
|
305
389
|
input.automationName,
|
|
306
390
|
input.factoryId,
|
|
307
391
|
);
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
392
|
+
const allocation = await resolvePromptVersionAllocation({
|
|
393
|
+
automationId: input.automationId,
|
|
394
|
+
orgId: input.orgId,
|
|
395
|
+
next: restoredSnapshot,
|
|
396
|
+
previous: previousSnapshot,
|
|
397
|
+
});
|
|
398
|
+
const resolvedVersion = allocation.promptVersion;
|
|
312
399
|
const configSavedAt = new Date().toISOString();
|
|
313
400
|
|
|
314
401
|
let content = preserveOperationalFrontmatterFields(
|
|
@@ -335,6 +422,20 @@ export async function restoreFactoryAutomationVersion(input: {
|
|
|
335
422
|
"factoryId",
|
|
336
423
|
input.factoryId,
|
|
337
424
|
);
|
|
425
|
+
// Versions saved before the stamp existed lack it; restoring one must not
|
|
426
|
+
// take it off the live file, nor tag a file that cannot pass the strict
|
|
427
|
+
// identity check.
|
|
428
|
+
const stamp = stampAutomationTriggerType(content, {
|
|
429
|
+
orgId: input.orgId,
|
|
430
|
+
triggerType: readFrontmatterValue(current.content, "triggerType"),
|
|
431
|
+
identityFrom: current.content,
|
|
432
|
+
});
|
|
433
|
+
content = stamp.content;
|
|
434
|
+
if (stamp.skipped) {
|
|
435
|
+
console.warn(
|
|
436
|
+
`[factory-automation-history] ${input.automationName} stays untagged after the restore because ${stamp.skipped}.`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
338
439
|
|
|
339
440
|
const insertedVersion = await insertFactoryAutomationVersionIfChanged({
|
|
340
441
|
automationId: input.automationId,
|
|
@@ -346,6 +447,7 @@ export async function restoreFactoryAutomationVersion(input: {
|
|
|
346
447
|
nextContent: content,
|
|
347
448
|
summary: input.summary,
|
|
348
449
|
source: "restore",
|
|
450
|
+
version: allocation.predecessorVersion ?? undefined,
|
|
349
451
|
});
|
|
350
452
|
|
|
351
453
|
let updated: Awaited<ReturnType<typeof resourcePutIfCurrent>> = null;
|
package/package.json
CHANGED