Skip to content

Commit 1ab1aef

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/app-crm-decision-routing-bug-aufqaj
2 parents 0d822c3 + dadb43f commit 1ab1aef

71 files changed

Lines changed: 1871 additions & 808 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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-automation": minor
4+
"@objectstack/plugin-approvals": minor
5+
"@objectstack/rest": patch
6+
"@objectstack/runtime": patch
7+
---
8+
9+
fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420)
10+
11+
A flow paused at an `approval` node, a deploy, then an approver clicking
12+
Approve: the request row flipped to `approved`, the UI toasted success — and
13+
the flow never moved. No next-stage request, no error, the record's mirrored
14+
status frozen mid-workflow. Approval flows pause for days by design, so a
15+
restart mid-flight is the normal case: every release could quietly zombify
16+
every in-flight approval, with the approvers none the wiser.
17+
18+
Durable suspended runs (#1518) had shipped and were not the missing piece. Two
19+
other things were.
20+
21+
**The wiring could enable a store over a table nobody had created.** Object
22+
registration and store activation resolve different services in different
23+
phases — `manifest` at `init()`, `objectql` at `start()` — and the plugin
24+
declared no ordering. Composed ahead of ObjectQL, `init()` found no `manifest`,
25+
warned, and continued; `start()` then attached the DB-backed store anyway. Every
26+
suspend failed with `no such table: sys_automation_run` into a log line nobody
27+
read, pauses silently stayed in memory, and the next restart lost them all.
28+
Now: `AutomationServicePlugin` declares `optionalDependencies:
29+
['com.objectstack.engine.objectql']` (order-if-present, per ADR-0116 — an
30+
engine-less kernel must still boot); a registration missed at `init()` is
31+
retried at `start()`, which still lands before ObjectQL's schema sync; the
32+
store is never attached when registration did not happen, and says so at
33+
**error** level instead of warning; the table is probed once at boot so a
34+
broken setup surfaces there rather than one failed write at a time; and a
35+
failed durable write of a paused run is logged at error — it is data loss in
36+
waiting, not a warning.
37+
38+
**A reported resume failure read as success.** `AutomationEngine.resume()`
39+
answers a lost run by *returning* `{ success: false }`, never by throwing.
40+
`ApprovalService` discarded that return value, and `decide()` counted only a
41+
thrown error as failure — so a decision against a dead run came back
42+
`resumed: true`, HTTP 200. Resume failures are now classified
43+
(`RUN_NOT_FOUND`, `STORE_UNAVAILABLE`, `RESUME_IN_PROGRESS`, joining
44+
`PERMISSION_DENIED` / `INVALID_SIGNAL`), so a run that is gone for good is
45+
distinguishable from a store that is merely unreachable, and the raw resume
46+
route maps them to 404 / 503 / 409.
47+
48+
Approvals acts on them. A new `AutomationEngine.hasSuspendedRun(runId)` — which
49+
reads the suspension store, unlike `getRun()`, and throws rather than answering
50+
`false` when the store is unreadable — pre-flights every flow-advancing
51+
operation (`decide`, `sendBack`, `resubmit`) **before its first write**, so the
52+
zombie half-state is never created rather than merely reported: the decision
53+
fails with `RESUME_TARGET_LOST` (HTTP 409) and the request stays actionable. A
54+
resume that fails after the decision is durable can no longer be undone, but it
55+
now throws `RESUME_FAILED` (HTTP 500) naming the stranded run instead of
56+
reporting success. A concurrent duplicate resume stays benign — the engine's
57+
idempotency guard is doing its job — and reports through the new optional
58+
`resumeError` field. Recall and revise-window cancellation stay non-fatal by
59+
design (they abandon the request), but log at error with the reason instead of
60+
swallowing it. Compositions with no automation engine attached are unaffected.
61+
62+
Existing zombie requests from affected deployments (already `approved`, run
63+
stranded) are not repaired by this change — `releaseDeadRunRequests` only
64+
sweeps requests that are still `pending`.

.changeset/datasource-config-driver-contract.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ package. So `config: { hostname: 'db.internal' }` (the key is `host`) was
1313
accepted in silence and the datasource connected to `localhost` while the parse,
1414
the save and the connection probe all reported success.
1515

16-
`DatasourceSchema` now parses `config` — and each `readReplicas` entry — against
16+
`DatasourceSchema` now parses `config` against
1717
the contract for the declared driver, and `DatasourceAdminService`
1818
(create/update/test, the Setup wizard's path) applies the same check. Both read
1919
one registry in `@objectstack/spec/data`, which also projects each contract to
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
'@objectstack/spec': major
3+
---
4+
5+
`datasource.readReplicas` is removed (#4468, ADR-0049 enforce-or-remove)
6+
7+
It described replica connections nothing ever opened. `ConnectableDatasource`
8+
and `DatasourceConnectionSpec` carry no replicas field, the driver factory never
9+
reads the key, and no query path distinguishes a read from a write — the
10+
platform has no read/write splitting at all, so every statement always went to
11+
the primary no matter what was declared here.
12+
13+
**Migration.**
14+
15+
| Wrote | Write instead |
16+
| --- | --- |
17+
| `readReplicas: [{ host: 'replica-a', … }]` | delete the key |
18+
| `replicas: [ … ]` (the alias) | delete the key |
19+
20+
There is no target to move to, because there is no read-replica routing to move
21+
to. If you need replica reads today, front them behind a single endpoint —
22+
pgpool, ProxySQL, an RDS reader endpoint — and point `config` at that endpoint.
23+
That is the one read-scaling path that works, and it worked before this key was
24+
removed too.
25+
26+
Run `os migrate meta --from 16` to strip it from your sources; the
27+
`datasource-read-replicas-removed` conversion emits one notice per datasource.
28+
Authoring it now fails the parse with the same prescription.
29+
30+
**Why this one is worth reading about.** #4410 closed the `datasource.config`
31+
gap and, in passing, extended the new per-driver validation over each
32+
`readReplicas` entry — reasonably, since replicas carry the same shape. The
33+
result was a slot that had every marker of a working feature: declared with a
34+
doc comment, `.strict()`-guarded against typos at the top level, and
35+
field-by-field validated against the driver's contract underneath. A replica
36+
block with a misspelt `hostname` was rejected by index, naming the canonical
37+
key.
38+
39+
None of that is evidence of a consumer, and all of it reads like one. That is
40+
the specific trap ADR-0049 exists for: rigor is cheap to add to a dead slot and
41+
expensive to distinguish from life. Two independent surfaces had drawn the
42+
wrong conclusion — this validation, and objectui's datasource preview, which
43+
rendered a "2 read replicas" pill confirming the config to the author while
44+
nothing routed a single read. The preview goes with the key (objectui side,
45+
same change); `packages/spec/liveness/README.md` has the standing rule it
46+
violated ("an authoring/preview renderer is NOT a runtime consumer").
47+
48+
Read-replica routing remains unbuilt. It is tracked as a feature request rather
49+
than left as a schema key that looks like one.

.changeset/unknown-key-strictness-data-step.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ Deliberately still tolerant:
2626
runtime shape the engine hands a handler. Strictness there would turn an
2727
engine-internal enrichment (as `provenance` was in #3712) into a breaking
2828
change for anyone parsing a context they were given.
29-
- `datasource.config` and `readReplicas` — per-driver by construction; the
30-
driver's own `configSchema` validates them.
29+
- `datasource.config` — per-driver by construction (a sqlite `filename` and a
30+
postgres `host`/`port` share no shape). Left open here and closed one level
31+
down instead: #4410 parses it against the contract for the declared driver.
32+
This bullet used to say "the driver's own `configSchema` validates them",
33+
which was not true when it was written — the field existed and nothing read
34+
it.
3135

3236
Errors are self-fixing: connection keys written one level too high (`host`,
3337
`port`, `filename`, `url`, …) are prescribed into `config`; a top-level
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/client": major
4+
"@objectstack/metadata-protocol": minor
5+
"@objectstack/runtime": minor
6+
---
7+
8+
refactor(spec,client,metadata-protocol,runtime)!: retire the workflow service slot — declared end to end, implemented nowhere (#4451)
9+
10+
The `workflow` slot was ADR-0078's silently-inert declaration at every layer at
11+
once: a `CoreServiceName` nothing ever registered or resolved (ADR-0115
12+
Evidence 5 — "no code in this repository resolves either slot", verified across
13+
both repositories), an `IWorkflowService` contract with zero implementations, a
14+
`WorkflowProtocol` whose three methods no code ever provided, a discovery
15+
`routes.workflow` field no builder could truthfully populate, and a
16+
`/api/v1/workflow` advertisement for a path no host ever mounted (the
17+
pre-#3586 `DEFAULT_DISPATCHER_ROUTES` already listed it among routes that
18+
never existed). The capability it promised is live elsewhere and has been for
19+
majors: record state machines are enforced by the `state_machine` validation
20+
rule, approvals are first-class flow nodes on the approvals runtime
21+
(ADR-0019), and record-triggered automation is lifecycle hooks +
22+
`record_change` flows (`service-automation`).
23+
24+
FROM → TO:
25+
26+
- `CoreServiceName 'workflow'` / `ServiceRequirementDef.workflow` /
27+
`CORE_SERVICE_PROVIDER['workflow']` → removed; there is no slot to fill.
28+
- `IWorkflowService` (`@objectstack/spec/contracts`) → removed; no
29+
implementation ever existed. Register nothing — use the mechanisms above.
30+
- `WorkflowProtocol` + `GetWorkflowConfigRequest/Response`,
31+
`WorkflowState`, `GetWorkflowStateRequest/Response`,
32+
`WorkflowTransitionRequest/Response` (`@objectstack/spec/api`) → removed,
33+
along with the seven published JSON schemas. Delete the import; nothing
34+
ever answered these shapes.
35+
- Discovery `routes.workflow` / `services.workflow` / `features.workflow`
36+
(metadata-protocol + runtime builders) → absent. A reader keying on them
37+
only ever saw `unavailable` / `false`; delete the read.
38+
- `RouterConfig.mounts.workflow` → removed; there was never a surface to
39+
mount at it.
40+
- `RestApiRouteCategory 'workflow'` → removed; categorize automation-adjacent
41+
routes as `'automation'`.
42+
- `@objectstack/client` re-exports of the four workflow types → removed with
43+
their source. (The `client.workflow.*` methods were already removed earlier
44+
in the v17 cycle — this retires the types they returned.)
45+
- Also removed: the stray `graphql` entry in `CORE_SERVICE_PROVIDER` and the
46+
`graphql: { route: '/graphql' }` discovery entry — `graphql` was never a
47+
`CoreServiceName`, and the dispatcher had already dropped `/graphql` as out
48+
of the product plan (#2462 follow-on).
49+
50+
The retirement kit: the `workflow-service-slot-retired` semantic migration
51+
(major 17) carries this prescription into `spec-changes.json`, the generated
52+
upgrade guide and the `spec_changes` MCP tool. These are TS/API surfaces and a
53+
discovery response field — never stored in stack metadata — so there is no
54+
load-path conversion and nothing for `os migrate meta` to rewrite; the
55+
21 `authorable-surface.json` baseline lines and 7 `json-schema.manifest.json`
56+
entries for the deleted schemas are dropped deliberately in the same change
57+
(the plugin-runtime precedent: a prescription nobody can receive is noise —
58+
nothing parses these shapes any more).

AGENTS.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,24 @@ this is mandatory, not a preference (Prime Directive #11), and a PreToolUse hook
106106
blocks edits made while on the shared `main` branch. Working in the shared `main`
107107
checkout is *not* a supported fallback: branches get switched and shared files —
108108
including ones you just wrote — get reset *under you* mid-task (a full session's
109-
work was silently reverted twice before this rule was enforced). Even inside your
110-
own worktree, operate defensively:
109+
work was silently reverted twice before this rule was enforced).
110+
111+
**Claim the issue BEFORE you write any code.** Assign it to yourself
112+
(`gh issue edit <n> --add-assignee @me`, or the `issue_write` MCP tool with
113+
`assignees`) as the *first* action of the task — before the worktree, before the
114+
first read. An unassigned issue reads as an open invitation, and several agents
115+
work this repo at once: two that both start on it burn the same hours twice and
116+
then race to land conflicting shapes for the same problem, which is worse than
117+
either one alone. If it is already assigned to someone else it is taken — pick
118+
another, or say so and ask; never reassign it to yourself.
119+
120+
The claim is also what makes the *finding* rule (Prime Directive #10) safe to
121+
follow. Once out-of-scope discoveries become issues, the issue list is a real
122+
queue other agents read, and a claim is the only thing separating "someone is on
123+
this" from "nobody has looked yet". File it unassigned when you are merely
124+
recording a finding; assign it at the moment you actually start.
125+
126+
Even inside your own worktree, operate defensively:
111127

112128
1. **Only touch the files your task needs.** Don't "fix" unrelated diffs,
113129
reverts, or other agents' in-flight edits, and don't try to manage the whole

CLAUDE.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
# CLAUDE.md
22

33
**[AGENTS.md](./AGENTS.md) is the source of truth for working in this repo — read it.**
4-
Its Prime Directives are binding. Do not rely on this file alone; the one rule that must
5-
never be missed is inlined here because missing it corrupts other agents' work.
4+
Its Prime Directives are binding. Do not rely on this file alone; the two rules that must
5+
never be missed are inlined here because missing either one wastes or corrupts other
6+
agents' work.
7+
8+
## ⛔ Claim the issue before you write any code
9+
10+
Assign the issue to yourself (`gh issue edit <n> --add-assignee @me`, or `issue_write`
11+
with `assignees`) as the **first action of the task** — before the worktree, before the
12+
first read. Several agents work this repo at once and an unassigned issue reads as an
13+
open invitation: two that both start on it burn the same hours twice, then race to land
14+
conflicting shapes for one problem. Already assigned to someone else? It is taken — pick
15+
another or ask; never reassign it to yourself. File findings unassigned when you are only
16+
recording them; assign at the moment you start.
617

718
## ⛔ Worktree-first — before your FIRST file edit (AGENTS.md Prime Directive #11)
819

content/docs/api/plugin-endpoints.mdx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,21 @@ Authenticate with email and password (better-auth's email sign-in route, mounted
2626

2727
The following endpoints become available when the corresponding plugin is installed and registered with the kernel. Use the discovery `services` map to check availability.
2828

29-
### Workflow (`/workflow`) — Plugin Required
29+
### Workflow (`/workflow`) — removed in v17
3030

3131
<Callout type="warn">
32-
Not yet mounted. These routes are declared in the API protocol but the core dispatcher registers no `/workflow` handler and no bundled plugin provides a `workflow` service (only an in-memory dev stub), so they return **404** today. `discovery.services.workflow` reports `unavailable` in a standard install.
32+
There is no workflow endpoint, and there is no `workflow` service slot. The
33+
three routes documented here were declared in the API protocol and served by
34+
nothing — no dispatcher handler, no plugin — so they 404'd for the whole life
35+
of the declaration. The slot, the `WorkflowProtocol` methods behind it and the
36+
discovery fields that reported it were all retired in v17 ([#4451](https://github.com/objectstack-ai/objectstack/issues/4451)).
37+
Use the live mechanisms instead: an object validation rule of type
38+
`state_machine` for lifecycle transitions, an `approval` flow node for human
39+
approval pauses (ADR-0019), and lifecycle hooks / `record_change` flows for
40+
record-triggered automation.
3341
</Callout>
3442

35-
| Method | Endpoint | Description |
36-
|:-------|:---------|:------------|
37-
| GET | `/workflow/:object/config` | Get workflow configuration |
38-
| GET | `/workflow/:object/:recordId/state` | Get record's workflow state |
39-
| POST | `/workflow/:object/:recordId/transition` | Execute state transition |
40-
41-
Approve/reject are **not** workflow routes (ADR-0019): approval is a flow node, and decisions are recorded on the approvals runtime via `POST /approvals/requests/:id/approve` and `POST /approvals/requests/:id/reject`.
43+
Approve/reject were never workflow routes (ADR-0019): approval is a flow node, and decisions are recorded on the approvals runtime via `POST /approvals/requests/:id/approve` and `POST /approvals/requests/:id/reject`.
4244

4345
### Automation (`/automation`) — Plugin Required
4446

0 commit comments

Comments
 (0)