howone 0.1.23 → 0.1.26

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 (36) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +215 -0
  3. package/templates/vite/.howone/skills/{howone-sdk → howone}/01-architect/02-manifest-codegen.md +67 -4
  4. package/templates/vite/.howone/skills/howone/02-database/01-schema-design.md +541 -0
  5. package/templates/vite/.howone/skills/howone/02-database/02-schema-operations.md +398 -0
  6. package/templates/vite/.howone/skills/howone/02-database/03-data-access-patterns.md +309 -0
  7. package/templates/vite/.howone/skills/howone/02-database/04-query-dsl-and-responses.md +237 -0
  8. package/templates/vite/.howone/skills/howone/02-database/05-ai-persistence-patterns.md +372 -0
  9. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/01-client-setup.md +58 -36
  10. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/02-entity-operations.md +67 -0
  11. package/templates/vite/.howone/skills/howone/03-sdk/03-auth.md +414 -0
  12. package/templates/vite/.howone/skills/howone/03-sdk/04-react-integration.md +191 -0
  13. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/07-ai-action-calls.md +168 -64
  14. package/templates/vite/.howone/skills/howone/03-sdk/08-extension-boundaries.md +226 -0
  15. package/templates/vite/.howone/skills/howone/04-ai/01-ai-capability-architecture.md +205 -0
  16. package/templates/vite/.howone/skills/howone/04-ai/02-workflow-contract-rules.md +426 -0
  17. package/templates/vite/.howone/skills/howone/04-ai/03-ai-sdk-handoff.md +234 -0
  18. package/templates/vite/.howone/skills/howone/04-ai/04-service-capability-catalog.md +281 -0
  19. package/templates/vite/.howone/skills/howone/04-ai/05-workflow-operations.md +256 -0
  20. package/templates/vite/.howone/skills/howone/04-ai/06-ai-feature-playbooks.md +296 -0
  21. package/templates/vite/.howone/skills/{howone-sdk → howone}/SKILL.md +29 -12
  22. package/templates/vite/.howone/skills/howone/agents/openai.yaml +4 -0
  23. package/templates/vite/package.json +1 -1
  24. package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +0 -126
  25. package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +0 -147
  26. package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +0 -96
  27. package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +0 -172
  28. package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +0 -616
  29. package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +0 -398
  30. package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +0 -1
  31. package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +0 -142
  32. package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +0 -169
  33. package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +0 -80
  34. package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +0 -4
  35. /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/05-file-upload.md +0 -0
  36. /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/06-raw-http.md +0 -0
@@ -0,0 +1,237 @@
1
+ # Query DSL And Responses
2
+
3
+ Use this reference when implementing list/detail pages, filters, sorting, pagination, or response
4
+ normalization for HowOne dynamic entities.
5
+
6
+ ## SDK Query Shape
7
+
8
+ ```ts
9
+ await howone.entities.Todo.query({
10
+ where: {
11
+ completed: false,
12
+ priority: { in: ['medium', 'high'] },
13
+ updatedDate: { gte: '2026-01-01T00:00:00.000Z' },
14
+ },
15
+ search: 'invoice',
16
+ page: { number: 1, size: 20 },
17
+ orderBy: { updatedDate: 'desc' },
18
+ include: ['owner'],
19
+ exactCount: true,
20
+ })
21
+ ```
22
+
23
+ Private owner-scoped list:
24
+
25
+ ```ts
26
+ await howone.entities.Todo.query.mine({
27
+ page: { number: 1, size: 50 },
28
+ orderBy: { updatedDate: 'desc' },
29
+ })
30
+ ```
31
+
32
+ Public list:
33
+
34
+ ```ts
35
+ await howone.public.entities.Article.query({
36
+ where: { status: 'published' },
37
+ orderBy: { publishedAt: 'desc' },
38
+ page: { number: 1, size: 20 },
39
+ })
40
+ ```
41
+
42
+ Public scoped:
43
+
44
+ ```ts
45
+ await howone.public.entities.QrProfile.queryScoped({
46
+ where: { ownerId, slug, active: true },
47
+ page: { number: 1, size: 1 },
48
+ })
49
+ ```
50
+
51
+ ## Operators
52
+
53
+ Supported field operators:
54
+
55
+ ```ts
56
+ type FieldOperator<T> = {
57
+ eq?: T
58
+ equals?: T
59
+ ne?: T
60
+ not?: T
61
+ gt?: T
62
+ gte?: T
63
+ lt?: T
64
+ lte?: T
65
+ contains?: string
66
+ like?: string
67
+ startsWith?: string
68
+ starts?: string
69
+ endsWith?: string
70
+ ends?: string
71
+ in?: T[]
72
+ notIn?: T[]
73
+ null?: boolean
74
+ empty?: boolean
75
+ exists?: boolean
76
+ }
77
+ ```
78
+
79
+ Examples:
80
+
81
+ ```ts
82
+ where: { status: 'published' }
83
+ where: { status: { eq: 'published' } }
84
+ where: { score: { gte: 80, lt: 100 } }
85
+ where: { category: { in: ['news', 'guide'] } }
86
+ where: { title: { contains: 'AI' } }
87
+ where: { deletedAt: { null: true } }
88
+ ```
89
+
90
+ ## Pagination
91
+
92
+ Use SDK page object:
93
+
94
+ ```ts
95
+ page: { number: 1, size: 20 }
96
+ ```
97
+
98
+ Response:
99
+
100
+ ```ts
101
+ type QueryResult<T> = {
102
+ items: T[]
103
+ page: {
104
+ number: number
105
+ size: number
106
+ total: number
107
+ totalPages: number
108
+ hasNext: boolean
109
+ hasPrev: boolean
110
+ }
111
+ traceId?: string | number
112
+ raw?: unknown
113
+ }
114
+ ```
115
+
116
+ Always render from a normalized array:
117
+
118
+ ```ts
119
+ const result = await howone.entities.Todo.query.mine(...)
120
+ const items = Array.isArray(result.items) ? result.items : []
121
+ ```
122
+
123
+ ## Sorting
124
+
125
+ SDK uses:
126
+
127
+ ```ts
128
+ orderBy: { updatedDate: 'desc' }
129
+ ```
130
+
131
+ Rules:
132
+
133
+ - Public sort fields must be in `access.public.allowedSorts`.
134
+ - Private sort fields should be in `performance.allowedSorts` and covered by indexes.
135
+ - Prefer `updatedDate` for recently changed lists and `createdDate` for creation history.
136
+ - Do not expose arbitrary public sort fields.
137
+
138
+ ## Include / Relations
139
+
140
+ Use `include` only for relation names declared in schema `relations`.
141
+
142
+ ```ts
143
+ await howone.entities.Article.query({
144
+ include: ['author'],
145
+ })
146
+ ```
147
+
148
+ Rules:
149
+
150
+ - Do not invent include names.
151
+ - Public includes must be safe for anonymous exposure.
152
+ - If a list needs a small display field frequently, consider denormalizing it instead of requiring include on every row.
153
+
154
+ ## Detail Reads
155
+
156
+ Authenticated:
157
+
158
+ ```ts
159
+ const item = await howone.entities.Todo.get(id)
160
+ const item = await howone.entities.Todo.getOrThrow(id)
161
+ ```
162
+
163
+ Public:
164
+
165
+ ```ts
166
+ const item = await howone.public.entities.Article.get(id)
167
+ ```
168
+
169
+ For public scoped detail, prefer `queryScoped` when the route naturally has scope fields like
170
+ `ownerId + slug`.
171
+
172
+ ## Response Field Names
173
+
174
+ SDK defaults to `caseStyle: 'camel'`, so responses are normalized toward camelCase where supported.
175
+ Still know the backend system field meanings:
176
+
177
+ | Backend concept | Common SDK field |
178
+ |---|---|
179
+ | record id | `id` |
180
+ | created date | `createdDate` or `created_date` depending case style |
181
+ | updated date | `updatedDate` or `updated_date` |
182
+ | owner | `createdById` or `created_by_id` |
183
+ | schema version id | `schemaVersionId` or `schema_version_id` |
184
+ | schema version number | `schemaVersionNumber` or `schema_version_number` |
185
+
186
+ Do not use `_id` in app code unless inspecting raw backend payloads.
187
+
188
+ ## Public Guardrails
189
+
190
+ Before writing public query code, check schema:
191
+
192
+ ```json
193
+ "public": {
194
+ "read": "list",
195
+ "allowedFilters": ["status", "slug"],
196
+ "allowedSorts": ["publishedAt"],
197
+ "defaultLimit": 20,
198
+ "maxLimit": 100
199
+ }
200
+ ```
201
+
202
+ Then only use allowed filters and sorts:
203
+
204
+ ```ts
205
+ // OK
206
+ where: { status: 'published' }
207
+ orderBy: { publishedAt: 'desc' }
208
+
209
+ // Not OK unless listed in access.public
210
+ where: { internalReviewState: 'approved' }
211
+ orderBy: { revenue: 'desc' }
212
+ ```
213
+
214
+ ## Search
215
+
216
+ Use `search` for broad text search only when the backend/schema supports the desired behavior:
217
+
218
+ ```ts
219
+ await howone.public.entities.Article.query({
220
+ search: query,
221
+ where: { status: 'published' },
222
+ })
223
+ ```
224
+
225
+ Do not use `search` as a substitute for required public scopes.
226
+
227
+ ## Common Mistakes
228
+
229
+ | Mistake | Fix |
230
+ |---|---|
231
+ | Rendering `res.data.data` from SDK query | SDK returns `QueryResult.items`; render `result.items`. |
232
+ | Using `_id` as record id | Use `id`. |
233
+ | Public query with unlisted filter | Add to `allowedFilters` or remove it. |
234
+ | Public query with unlisted sort | Add to `allowedSorts` or change sorting. |
235
+ | Passing owner filters in authenticated `own` queries | Use `query.mine()` and omit owner fields. |
236
+ | Using include without schema relation | Add relation first or remove include. |
237
+ | No pagination on list pages | Always pass `page` and respect `maxLimit`. |
@@ -0,0 +1,372 @@
1
+ # AI Persistence Patterns
2
+
3
+ Use this reference when an AI workflow output must become durable product data: generation history,
4
+ saved results, analysis reports, retryable jobs, share pages, or user libraries.
5
+
6
+ AI workflow contracts describe **how to produce an output**. Entity schemas describe **what the app
7
+ persists, lists, edits, shares, and reloads**. Do not merge those two responsibilities.
8
+
9
+ ## Core Rule
10
+
11
+ ```text
12
+ AI capability outputSchema != database entity schema
13
+ ```
14
+
15
+ The output schema is the workflow return contract. It can contain transient execution details,
16
+ intermediate data, model metadata, or provider-shaped structures. The database schema is a product
17
+ contract. It should store only fields that the app needs after refresh, across sessions, or on public
18
+ pages.
19
+
20
+ For every AI feature, ask:
21
+
22
+ | Question | Persist? | Where |
23
+ |---|---:|---|
24
+ | Must the user see this after refresh? | yes | Entity field |
25
+ | Must it appear in history/library/search? | yes | Entity field + index if queried |
26
+ | Is it only needed while streaming/running? | no | Local UI state / runtime event |
27
+ | Is it provider debug data? | usually no | Logs, not entity data |
28
+ | Is it needed to retry or resume? | yes | Entity field |
29
+ | Is it sensitive model/provider metadata? | usually no | Avoid public entity fields |
30
+
31
+ ## Recommended Flow
32
+
33
+ For long-running or user-visible AI operations, create a pending record before calling the workflow.
34
+
35
+ ```ts
36
+ import { runAiActionAndPersist } from '@howone/sdk'
37
+
38
+ await runAiActionAndPersist({
39
+ entity: howone.entities.Generation,
40
+ input: { prompt },
41
+ createPending: (input) => ({
42
+ prompt: input.prompt,
43
+ status: 'pending',
44
+ requestedAt: new Date().toISOString(),
45
+ }),
46
+ run: (input) => howone.ai.generateImage.run(input),
47
+ mapCompleted: ({ output }) => ({
48
+ status: 'completed',
49
+ resultUrl: output.imageUrl,
50
+ completedAt: new Date().toISOString(),
51
+ }),
52
+ mapFailed: ({ error }) => ({
53
+ status: 'failed',
54
+ errorMessage: error instanceof Error ? error.message : 'Generation failed',
55
+ }),
56
+ })
57
+ ```
58
+
59
+ Why pending-first:
60
+
61
+ - refresh can show an in-progress item instead of losing the request;
62
+ - failure can be displayed in history;
63
+ - retry can reuse the original prompt/options;
64
+ - support/debug can identify which input produced the failed state;
65
+ - UI can render from persisted data instead of assuming local state survived.
66
+
67
+ For very fast, disposable AI actions, persistence may be unnecessary. Do not create an entity just
68
+ because an AI workflow exists.
69
+
70
+ ## Status Fields
71
+
72
+ Every persisted AI job/history entity should have a status field.
73
+
74
+ Recommended:
75
+
76
+ ```json
77
+ {
78
+ "status": {
79
+ "type": "string",
80
+ "description": "pending | running | completed | failed | canceled",
81
+ "default": "pending"
82
+ }
83
+ }
84
+ ```
85
+
86
+ Use stable string values:
87
+
88
+ | Status | Meaning |
89
+ |---|---|
90
+ | `pending` | Record exists, workflow has not produced final output. |
91
+ | `running` | Optional if the app receives a running state after submission. |
92
+ | `completed` | Output fields are valid for display. |
93
+ | `failed` | `errorMessage` or failure fields explain the failure. |
94
+ | `canceled` | User/system canceled and no final output should be expected. |
95
+
96
+ Rules:
97
+
98
+ - Do not infer completion only from `resultUrl` or another output field.
99
+ - Keep failed records when the product has history or retry UX.
100
+ - If the UI shows a spinner from persisted data, it must also handle stale `pending/running` records.
101
+
102
+ ## Minimal Generation History Schema
103
+
104
+ Use for image/text/report/music/video generation history.
105
+
106
+ ```json
107
+ {
108
+ "name": "Generation",
109
+ "type": "object",
110
+ "visibility": "private",
111
+ "properties": {
112
+ "prompt": { "type": "string" },
113
+ "status": { "type": "string", "default": "pending" },
114
+ "resultUrl": { "type": ["string", "null"], "default": null },
115
+ "resultText": { "type": ["string", "null"], "default": null },
116
+ "errorMessage": { "type": ["string", "null"], "default": null },
117
+ "requestedAt": { "type": "date" },
118
+ "completedAt": { "type": ["date", "null"], "default": null }
119
+ },
120
+ "required": ["prompt", "status", "requestedAt"],
121
+ "access": {
122
+ "authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
123
+ "public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
124
+ },
125
+ "indexes": [
126
+ { "name": "owner_updated", "fields": ["updatedDate"], "scope": "owner" },
127
+ { "name": "owner_status_updated", "fields": ["status", "updatedDate"], "scope": "owner" }
128
+ ],
129
+ "performance": {
130
+ "defaultLimit": 20,
131
+ "maxLimit": 100,
132
+ "allowedSorts": ["updatedDate", "requestedAt"]
133
+ },
134
+ "presentation": {
135
+ "titleField": "prompt",
136
+ "subtitleField": "status"
137
+ }
138
+ }
139
+ ```
140
+
141
+ Use separate result fields instead of one opaque `result` object when the UI lists or filters them.
142
+ Use an object field only for genuinely nested, product-level structured results.
143
+
144
+ ## Analysis Report Schema
145
+
146
+ Use when AI returns a structured report that users browse later.
147
+
148
+ ```json
149
+ {
150
+ "name": "AnalysisReport",
151
+ "type": "object",
152
+ "visibility": "private",
153
+ "properties": {
154
+ "sourceTitle": { "type": "string" },
155
+ "sourceUrl": { "type": ["string", "null"], "default": null },
156
+ "status": { "type": "string", "default": "pending" },
157
+ "summary": { "type": ["string", "null"], "default": null },
158
+ "insights": { "type": "array", "default": [] },
159
+ "score": { "type": ["number", "null"], "default": null },
160
+ "errorMessage": { "type": ["string", "null"], "default": null },
161
+ "requestedAt": { "type": "date" },
162
+ "completedAt": { "type": ["date", "null"], "default": null }
163
+ },
164
+ "required": ["sourceTitle", "status", "requestedAt"],
165
+ "access": {
166
+ "authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
167
+ "public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
168
+ }
169
+ }
170
+ ```
171
+
172
+ Mapping rule:
173
+
174
+ ```ts
175
+ const output = await howone.ai.analyzeDocument.run(input)
176
+
177
+ await howone.entities.AnalysisReport.update(report.id, {
178
+ status: 'completed',
179
+ summary: output.summary,
180
+ insights: output.insights,
181
+ score: output.score,
182
+ completedAt: new Date().toISOString(),
183
+ })
184
+ ```
185
+
186
+ Do not save the whole workflow envelope unless the entity has an explicitly designed field for that
187
+ envelope and the product needs it.
188
+
189
+ ## Public AI Result Share Page
190
+
191
+ Use when a user can share one generated result publicly.
192
+
193
+ Recommended split:
194
+
195
+ - private `Generation` entity for user history and edits;
196
+ - public or scoped `SharedGeneration` entity for anonymous viewing.
197
+
198
+ Why split:
199
+
200
+ - private history may include prompts, failures, drafts, and internal metadata;
201
+ - public page should expose only curated fields;
202
+ - public access rules stay simple and auditable;
203
+ - unsharing can delete or deactivate the shared record without destroying private history.
204
+
205
+ Public scoped schema:
206
+
207
+ ```json
208
+ {
209
+ "name": "SharedGeneration",
210
+ "type": "object",
211
+ "visibility": "public",
212
+ "properties": {
213
+ "shareId": {
214
+ "type": "string",
215
+ "autoGenerate": { "strategy": "uuid" }
216
+ },
217
+ "title": { "type": "string" },
218
+ "resultUrl": { "type": "string" },
219
+ "active": { "type": "boolean", "default": true },
220
+ "sourceGenerationId": { "type": "string" }
221
+ },
222
+ "required": ["shareId", "title", "resultUrl", "active", "sourceGenerationId"],
223
+ "access": {
224
+ "authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
225
+ "public": {
226
+ "read": "scoped",
227
+ "create": "none",
228
+ "update": "none",
229
+ "delete": "none",
230
+ "requiredScopes": ["shareId"],
231
+ "allowedFilters": ["shareId", "active"],
232
+ "allowedSorts": ["updatedDate"],
233
+ "defaultLimit": 1,
234
+ "maxLimit": 1
235
+ }
236
+ },
237
+ "indexes": [
238
+ { "name": "share_id_unique", "fields": ["shareId"], "unique": true },
239
+ { "name": "owner_updated", "fields": ["updatedDate"], "scope": "owner" }
240
+ ]
241
+ }
242
+ ```
243
+
244
+ Public page:
245
+
246
+ ```ts
247
+ const result = await howone.public.entities.SharedGeneration.queryScoped({
248
+ where: { shareId, active: true },
249
+ page: { number: 1, size: 1 },
250
+ })
251
+
252
+ const shared = result.items[0] ?? null
253
+ ```
254
+
255
+ Rules:
256
+
257
+ - Do not expose private prompt fields unless the product explicitly wants that.
258
+ - Use `active` or a deletion flow for unshare.
259
+ - Keep public `maxLimit` at `1` for one-share pages.
260
+
261
+ ## Retry Design
262
+
263
+ If the product has retry, store enough input fields to rebuild the workflow request.
264
+
265
+ Persist:
266
+
267
+ - user prompt or source content reference;
268
+ - selected mode/model/style options that affect output;
269
+ - uploaded file IDs/URLs needed by the workflow;
270
+ - status and failure message;
271
+ - timestamps.
272
+
273
+ Do not persist:
274
+
275
+ - temporary UI component state;
276
+ - auth/session/token values;
277
+ - raw streaming chunks;
278
+ - provider secrets;
279
+ - hidden prompt text that should stay server-side;
280
+ - large binary content when file upload should store a URL or file id.
281
+
282
+ Retry example:
283
+
284
+ ```ts
285
+ const old = await howone.entities.Generation.getOrThrow(id)
286
+
287
+ const retry = await howone.entities.Generation.create({
288
+ prompt: old.prompt,
289
+ status: 'pending',
290
+ requestedAt: new Date().toISOString(),
291
+ })
292
+
293
+ const output = await howone.ai.generateImage.run({
294
+ prompt: old.prompt,
295
+ })
296
+
297
+ await howone.entities.Generation.update(retry.id, {
298
+ status: 'completed',
299
+ resultUrl: output.imageUrl,
300
+ completedAt: new Date().toISOString(),
301
+ })
302
+ ```
303
+
304
+ Prefer creating a new retry record when the product is history-oriented. Prefer updating the same
305
+ record only when the product treats retry as replacing the original attempt.
306
+
307
+ ## Resume Design
308
+
309
+ On page load, query persisted records instead of relying on local state:
310
+
311
+ ```ts
312
+ const history = await howone.entities.Generation.query.mine({
313
+ where: { status: { in: ['pending', 'running', 'completed', 'failed'] } },
314
+ orderBy: { updatedDate: 'desc' },
315
+ page: { number: 1, size: 20 },
316
+ })
317
+ ```
318
+
319
+ Then:
320
+
321
+ - render `completed` from output fields;
322
+ - render `failed` from `errorMessage`;
323
+ - render `pending/running` as in progress only if the app has a way to poll status;
324
+ - mark stale pending records as failed/canceled when product rules define a timeout.
325
+
326
+ Do not leave indefinite pending rows with no recovery path.
327
+
328
+ ## Field Mapping Checklist
329
+
330
+ Before implementing persistence, write the mapping explicitly:
331
+
332
+ ```text
333
+ workflow input.prompt -> Generation.prompt
334
+ workflow input.style -> Generation.style
335
+ workflow output.imageUrl -> Generation.resultUrl
336
+ workflow output.caption -> Generation.resultText
337
+ workflow error.message -> Generation.errorMessage
338
+ runtime request started -> Generation.requestedAt
339
+ runtime request completed -> Generation.completedAt
340
+ ```
341
+
342
+ If a workflow output field has no mapping, decide whether it is intentionally transient or whether
343
+ the entity schema is missing a product field.
344
+
345
+ ## Access Checklist
346
+
347
+ Choose access based on product behavior:
348
+
349
+ | Product behavior | Entity access |
350
+ |---|---|
351
+ | User-only private generation history | authenticated own, public none |
352
+ | Team/shared authenticated library | authenticated all or future role-scoped model |
353
+ | Anonymous public gallery | public list with safe fields only |
354
+ | One public share link | public scoped with share id |
355
+ | Public submission to AI queue | public create only if abuse constraints are handled |
356
+
357
+ Never make the main generation history public just to support a public share page. Create a scoped
358
+ share entity or a curated public entity.
359
+
360
+ ## Common Mistakes
361
+
362
+ | Mistake | Fix |
363
+ |---|---|
364
+ | Treating `outputSchema` as the entity schema | Design product persistence separately. |
365
+ | Saving raw workflow envelopes | Map only fields the product needs after refresh. |
366
+ | No status field | Add `status` and explicit failure fields. |
367
+ | Creating history only after success | Create pending first when history/resume matters. |
368
+ | Losing failures | Persist `failed` state and `errorMessage`. |
369
+ | Publicly exposing private prompt/history | Use a separate public scoped/share entity. |
370
+ | Retrying without stored inputs | Persist the inputs/options needed for retry. |
371
+ | Rendering from local state only | Reload from entity queries on page load. |
372
+ | Leaving stale pending forever | Add timeout/recovery behavior in product logic. |