Skip to content

Commit 4ffb37d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-ev0wwj
2 parents f189880 + f6adf48 commit 4ffb37d

62 files changed

Lines changed: 1989 additions & 604 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/components': patch
3+
'@object-ui/plugin-list': patch
4+
'@object-ui/plugin-detail': patch
5+
'@object-ui/i18n': patch
6+
---
7+
8+
fix(ui): four KPI tiles sit two across on a phone, a calendar's footer says its count is the loaded dates', and the record not-found page says the record may also be one you cannot read
9+
10+
Clause-②: no
11+
Metadata-protocol: none
12+
13+
Three console rows from a full HotCRM browser pass:
14+
15+
- **A layout `grid` with a bare `columns` of 4 or more draws two tiles across on a phone** (below `sm`), then the authored count from `md`. A home page's KPI strip (`columns: 4` over `object-metric` tiles) took a row per tile and filled the first screen; it now reads as the dashboard's own phone layout for metric tiles. A bare 2 or 3 still stacks on a phone and pairs from `sm`, and an authored breakpoint object (`columns: { xs: 1, md: 4 }`) is drawn as written.
16+
- **A calendar view's footer says `N records loaded for these dates`.** The calendar fetches the window of dates it draws, so the footer's count is that window's records, not the view's total; it read "N records", the words a grid uses for the whole view. A grid, a board and a calendar over rows handed in whole keep "N records". New count family `list.calendarRecordCount` in all ten packs.
17+
- **The record not-found page says the record "does not exist, may have been deleted, or you may not have access to it".** The server answers a record the reader may not see the same as a missing one (`404 RECORD_NOT_FOUND`, so existence is never revealed), so the page cannot tell the two apart and now names both. `empty.recordNotFoundDescription` and `detail.recordNotFoundDescription` change in all ten packs.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
'@object-ui/i18n': patch
4+
'@object-ui/app-shell': patch
5+
---
6+
7+
feat(plugin-detail)!: the record page's related panels lose collapse — the header opens the whole list, ⠿ + ⋯ on hover, ⋯ holds Hide alone, empty panels fold into one line, and the stored arrangement drops `collapsed`
8+
9+
Clause-②: yes (narrowing)
10+
Metadata-protocol: none
11+
12+
<!-- adr-0087: not-required (runtime-interface-only ui/packages/plugin-detail/src/synth/relatedArrangement.ts#RelatedArrangement, ui/packages/plugin-detail/src/synth/relatedArrangement.ts#ResolvedRelatedArrangement, ui/packages/plugin-detail/src/synth/relatedArrangement.ts#RelatedArrangementEdit, ui/packages/i18n/src/locales/en.ts#TranslationKeys) a reader's per-browser layout preference and its edit vocabulary, and the key type of the console's own translation catalogue: no Zod schema, no spec declaration and no stored metadata reads them, so the compiler is the channel that reaches every caller; the stored preference moves to version 2 and a version-1 record reads as no choice -->
13+
14+
The record page's related column is a stack of same-shaped panels (record page v4).
15+
16+
- **The panel header** is the title, then the count chip. The header row (or the chip) opens the panel's whole list; on hover the title's end shows › and its tooltip reads "View all N". On hover or focus the right end shows ⠿ (drag to reorder; focused, ⌥↑ / ⌥↓ move the panel), + and ⋯, which now holds Hide and nothing else. There is no collapse and no ⤢, and nothing is reserved on the left.
17+
- **Empty panels** (0 rows) fold into one line after the others: "N more empty lists · name · … · Expand". Expand draws them again in their places. The Related tab of an authored page keeps its own line.
18+
- **The compact row's first column** skips the foreign key that points back at the record on the page: a product's line items, whose name field is the product, are named by their next column.
19+
- **The stored arrangement** (`record.related.arrangement`, per reader × object) is `{ v, order, hidden, basedOn, seen }`. `collapsed` is removed with no compatibility read: a reader's earlier record reads as no choice, and the panels show in the default order.
20+
21+
**BREAKING** (`@object-ui/plugin-detail` and `@object-ui/i18n`, TypeScript only): `@object-ui/i18n`'s exported `TranslationKeys` loses the four `detail.relatedCard*` keys named below (a `t()` call with one of them still compiles, its key being a `string`, and reads the key back); and `RelatedArrangement` and `ResolvedRelatedArrangement` no longer carry `collapsed`, `RelatedArrangementEdit` no longer has the `collapse` kind, and `resolveRelatedArrangement` / `editRelatedArrangement` no longer take a last `defaultCollapsed` argument. The only caller is the console's own arranged column. A caller still passing one gets a compile error naming it; the fix is to drop it, since a panel has no collapsed state to set. `RELATED_ARRANGEMENT_RECORD_VERSION` (2) is new.
22+
23+
- The i18n keys `detail.relatedCardMoveUp`, `detail.relatedCardMoveDown`, `detail.relatedCardCollapse` and `detail.relatedCardCount` leave every pack; `detail.relatedPanelViewAll`, `detail.relatedPanelReorder`, `detail.relatedPanelReorderHint` and `detail.relatedEmptyFold` arrive in all ten.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
'@objectstack/spec': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
fix(service-automation)!: a resume of a run parked on a `map` node, addressed by the parent's run id while the in-flight item's child run is still suspended, is refused with a code naming the child run id, instead of completing the parent past an item that never ran (#27877)
8+
9+
Clause-②: yes (narrowing) — `resume(runId)` on a run whose resume chain reaches a `map` link with a still-suspended item child, which answered success before, is refused, with a new wire code `RESUME_CHILD_PENDING` (a new member of the `AutomationResult.code` union, the `TriggerFlowResponseSchema` `code` enum and the ADR-0112 ledger).
10+
Metadata-protocol: none
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key is added, removed, renamed or re-shaped: the one protocol change is a new member of a response code enum, which no metadata author writes. A resume the engine accepted is refused before any state is touched, so there is no stored or authored shape for a migration to rewrite and no tombstone. -->
13+
14+
**BREAKING** accept-set narrowing at `AutomationEngine.resume` (`@objectstack/service-automation`), served by the generic resume route (`POST /automation/:name/runs/:runId/resume`) and the MCP `resume_run` tool (`@objectstack/runtime`), with its code added to the contract (`@objectstack/spec`).
15+
16+
- **Before.** A run parked on a `map` node, resumed by its own id while the item's child run was parked on a pause open to the generic route (a `screen`, a `wait`, any node declaring `resumeAuthority: 'any'`), re-ran the map node. The map had already counted the in-flight item as started, so it moved past it: the resume answered success, the parent's run row was `completed`, and the item child stayed in `listSuspendedRuns()` with nothing left to continue into.
17+
- **Now.** That resume is refused with the new code `RESUME_CHILD_PENDING` (409 on both doors), and the message names the item run id to resume instead. Nothing moves: the parent stays parked on the item, the map's progress is unchanged, and the item child stays resumable. The same holds when a `subflow` link leads down to such a map: the refusal names the item run of the deepest map link.
18+
- **Unchanged.** Resuming the item run continues the map exactly as before, through the engine's own up-bubble. A parent parked on a `subflow` node still delegates the signal to its child. An item parked on a node only its owning service may resume still answers `PERMISSION_DENIED` (403) at the parent's id. A `map` whose item run is gone (cancelled, or never persisted) is not refused.
19+
- **New code.** `RESUME_CHILD_PENDING` joins `AutomationResult.code`, `TriggerFlowResponseSchema.data.code` and the error-code ledger under `@objectstack/service-automation`; the runtime resume door maps it to 409. It is not `RESUME_IN_PROGRESS`: that one is benign and needs nothing from the caller, this one is answered only by resuming a different run id. A client that switches exhaustively over the code union gains one member.
20+
- **Who sees it.** A caller that resumes a batch flow by the parent run id it launched with, while an item is parked on a screen or a wait.
21+
22+
**The remedy.** Resume the item run the refusal names (also listed by `listSuspendedRuns()`, the run whose context carries the parent's id); the parent continues when the map's last item completes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(console): a full-page record form opened on the app's name address stays in the app after Save
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A record's full-page form (`/apps/APP/OBJECT/record/ID/edit` or `/apps/APP/OBJECT/new`) opened by a pasted link, a bookmark or a new tab on the app's **name** (rather than its package id) saved, then went Back out of the console: the page the tab showed before came back and the save toast was lost. The console moves a name address to the package-id one on load, and that move made the page believe a console page was behind it. Save and Cancel now go Back only when the console itself navigated to the form; otherwise an edit lands on the record and a new record on the object's list, inside the app, with one toast. A form opened from a list, a view or a record page still returns to that page.

‎content/docs/references/api/automation-api.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ const result = AutomationApiErrorCode.parse(data);
347347

348348
| Property | Type | Required | Description |
349349
| :--- | :--- | :--- | :--- |
350-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +352 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
350+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +353 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
351351
| **message** | `string` | ✅ | The inner refusal's own diagnostic text, verbatim, without the node and operation prefixes the run's failure text adds |
352352

353353
### Allowed Values: `FlowInnerRefusal.code`
@@ -631,6 +631,7 @@ const result = AutomationApiErrorCode.parse(data);
631631
* `REQUEST_NOT_FOUND`
632632
* `RESEED_NO_ROWS`
633633
* `RESEED_SKIPPED`
634+
* `RESUME_CHILD_PENDING`
634635
* `RESUME_FAILED`
635636
* `RESUME_IN_PROGRESS`
636637
* `RESUME_TARGET_LOST`

‎content/docs/references/api/contract.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const result = ApiErrorSchema.parse(data);
2828

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
31-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +352 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
31+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +353 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3232
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
3333
| **message** | `string` | ✅ | Readable error message |
3434
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -319,6 +319,7 @@ const result = ApiErrorSchema.parse(data);
319319
* `REQUEST_NOT_FOUND`
320320
* `RESEED_NO_ROWS`
321321
* `RESEED_SKIPPED`
322+
* `RESUME_CHILD_PENDING`
322323
* `RESUME_FAILED`
323324
* `RESUME_IN_PROGRESS`
324325
* `RESUME_TARGET_LOST`

‎content/docs/references/api/error-code-ledger.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ const result = ErrorCode.parse(data);
486486
* `REQUEST_NOT_FOUND`
487487
* `RESEED_NO_ROWS`
488488
* `RESEED_SKIPPED`
489+
* `RESUME_CHILD_PENDING`
489490
* `RESUME_FAILED`
490491
* `RESUME_IN_PROGRESS`
491492
* `RESUME_TARGET_LOST`

‎packages/runtime/src/domains/automation-resume-refusal-codes.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,14 @@ function makeDispatcher(resumeResult: AutomationResult) {
5858
const ROWS = [...RESUME_REFUSAL_ROWS].map(([code, row]) => [code, row.status] as const);
5959

6060
describe('#21724 — the resume-refusal rows', () => {
61-
it('the table is the six engine refusals the contract publishes', () => {
61+
it('the table is the seven engine refusals the contract publishes', () => {
6262
// A floor, so an emptied or truncated table cannot pass the per-row
6363
// pins below vacuously.
6464
expect(ROWS.map(([code]) => code).sort()).toEqual([
6565
'INVALID_SCREEN_INPUT',
6666
'INVALID_SIGNAL',
6767
'PERMISSION_DENIED',
68+
'RESUME_CHILD_PENDING',
6869
'RESUME_IN_PROGRESS',
6970
'RUN_NOT_FOUND',
7071
'STORE_UNAVAILABLE',

‎packages/runtime/src/domains/automation.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2612,6 +2612,7 @@ export const RESUME_REFUSAL_ROWS: ReadonlyMap<string, { status: number; fallback
26122612
['RUN_NOT_FOUND', { status: 404, fallback: 'No such suspended run' }],
26132613
['STORE_UNAVAILABLE', { status: 503, fallback: 'Suspended-run store unavailable' }],
26142614
['RESUME_IN_PROGRESS', { status: 409, fallback: 'Run is already being resumed' }],
2615+
['RESUME_CHILD_PENDING', { status: 409, fallback: 'Resume the pending child run instead' }],
26152616
]);
26162617

26172618
/**
@@ -2625,7 +2626,7 @@ export const RESUME_REFUSAL_ROWS: ReadonlyMap<string, { status: number; fallback
26252626
* message on either door. This used to be inline in the REST arm. It moved here
26262627
* unchanged, and the arm's wire answers are byte-identical.
26272628
*
2628-
* The six coded rows are REFUSALS the engine made BEFORE consuming the
2629+
* The seven coded rows are REFUSALS the engine made BEFORE consuming the
26292630
* suspension, so the run is still parked and the caller can retry (the codes
26302631
* and why each has its status are listed at the REST arm).
26312632
*
@@ -3486,6 +3487,10 @@ export async function handleAutomationRequest(deps: DomainHandlerDeps, path: str
34863487
// existence is unknown; the same call is expected
34873488
// to work once it recovers (#4420)
34883489
// RESUME_IN_PROGRESS → 409, a concurrent resume already has this run
3490+
// RESUME_CHILD_PENDING → 409, the run waits on a `map` item whose
3491+
// child run is still suspended; the engine's
3492+
// message names that child run id to resume
3493+
// instead, and nothing moved (#27877)
34893494
// A result with NO code and `success: false` consumed its pause and
34903495
// ran: 400 `FLOW_FAILED` (#8684), whose details carry the engine's
34913496
// own verdict since #15221 — `status: 'stranded'` + `repairable`

‎packages/runtime/src/mcp-resume-run.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ describe('MCP resumeRun — (d) the engine\'s answers match the REST resume door
517517
['RUN_NOT_FOUND', { success: false, code: 'RUN_NOT_FOUND', error: "Suspended node 'collect' no longer exists" }, 'RUN_NOT_FOUND'],
518518
['STORE_UNAVAILABLE', { success: false, code: 'STORE_UNAVAILABLE' }, 'STORE_UNAVAILABLE'],
519519
['RESUME_IN_PROGRESS', { success: false, code: 'RESUME_IN_PROGRESS', error: 'Run is already being resumed' }, 'RESUME_IN_PROGRESS'],
520+
['RESUME_CHILD_PENDING', { success: false, code: 'RESUME_CHILD_PENDING', error: "Resume run 'run_child' instead" }, 'RESUME_CHILD_PENDING'],
520521
['FLOW_FAILED (stranded)', {
521522
success: false, error: 'tail blew up', status: 'stranded',
522523
errorMessage: 'Please contact support', summary: { nodes: [] },

0 commit comments

Comments
 (0)