Skip to content

Commit 32f6241

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16746-connect-agent-account-nav
2 parents 0ae6542 + 155b875 commit 32f6241

80 files changed

Lines changed: 2253 additions & 1268 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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/service-messaging': minor
3+
---
4+
5+
`HttpDispatcher` reaps once per tick instead of once per partition, backs off while `sys_http_delivery` is idle, and `enqueueHttp()` / `redeliverHttp()` wake it (#17623)
6+
7+
**What an idle dispatcher cost.** Against an EMPTY `sys_http_delivery` outbox every tick walked `partitionCount` partitions (default 8) and ran `claim()` in each — and each claim opened with the environment-wide visibility-timeout reap before its candidate SELECT. Measured on a real `ObjectQL` + `SqlDriver`: **16 SQL statements a tick, 8 of them the identical reap UPDATE**, on a fixed 500 ms `setInterval` that never let up, one loop per warm kernel. It is the shape #17610 removed from `NotificationDispatcher`, still running beside it. On remote Turso every statement is an HTTP round trip.
8+
9+
**Now:**
10+
11+
- **The reap runs once per tick**, before any claim — an idle tick is `1 + partitionCount` = 9 statements. Its predicate names no partition, so one run returns every claim that had expired when the tick began; a claim that expires during the tick is returned by the next one. A crashed node's `in_flight` rows are still recovered within one tick of `claimTtlMs` passing, and a claim is still never re-taken before its TTL.
12+
- **The loop backs off while idle.** Every tick that claims nothing doubles the delay to the next, from `intervalMs` up to `maxIdleIntervalMs` (default 30 s, the notification dispatcher's default). A tick that claims work snaps back to `intervalMs`. With the defaults, ten idle minutes are 24 ticks and 216 statements instead of 1,201 ticks and 19,216.
13+
- **`MessagingServicePlugin`'s `dispatchMaxIdleIntervalMs` sets the ceiling for both dispatchers**, the way `dispatchIntervalMs` and `partitionCount` already govern both.
14+
- **Writes in this process wake the dispatcher.** `MessagingService.setHttpOutbox(outbox, { onEnqueued })` fires after an `enqueueHttp()` that enqueues a delivery — not one that parks an undeliverable record, which is `dead` on arrival — and after a `redeliverHttp()`. The plugin points it at the new `HttpDispatcher.wake()`, which ticks immediately, or once more right after a tick already in flight.
15+
16+
**Latency bound.** A delivery enqueued or redelivered in the process that runs the dispatcher goes out on the tick `wake()` starts. While idle, work nobody announces is noticed within one backed-off interval, at most `maxIdleIntervalMs` (30 s by default):
17+
18+
- a retry coming due is attempted less than `min(its delay + intervalMs, maxIdleIntervalMs)` late, because the backoff restarts from `intervalMs` at the attempt that scheduled it;
19+
- a row enqueued by a process that does not run this dispatcher;
20+
- a crashed node's expired claim, recovered within `claimTtlMs` + `maxIdleIntervalMs` (about 35 s at defaults, where it was about 5.5 s).
21+
22+
Set `dispatchMaxIdleIntervalMs` to `dispatchIntervalMs` to keep the fixed interval.
23+
24+
**Contract additions — all optional, nothing to change on upgrade.** `IHttpOutbox` gains an optional `reap(opts: HttpReapOptions)` — the visibility-timeout recovery `claim()` already opens with, as a method of its own — and `HttpClaimOptions` gains an optional `skipReap`. Both built-in stores (`SqlHttpOutbox`, `MemoryHttpOutbox`) implement them. A custom outbox without `reap()` keeps working as it is: the dispatcher probes for the method and, when it is absent, lets each claim reap as before — correct, at the old per-claim cost. Direct callers of `claim()` are unaffected: without `skipReap` they reap exactly as before. Also new: `HttpDispatcher.wake()`, the dispatcher's `maxIdleIntervalMs` option, the `HttpReapOptions` type, and `MessagingService.setHttpOutbox`'s optional second argument.
25+
26+
**One loop, not two copies.** The timer loop — idle backoff, collapsing wakes into one follow-up tick, `stop()` — moved out of `NotificationDispatcher` into a module both dispatchers share. `NotificationDispatcher`'s behaviour and public surface are unchanged; its #17610 tests pass as they were.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
'@objectstack/metadata-protocol': minor
5+
---
6+
7+
**BREAKING** — retire the `type: 'page'` list-view mount and its `pageName` binding.
8+
9+
A list view could declare `type: 'page'` and name a published page in `pageName`,
10+
and the view was to render nothing of its own and delegate to the page renderer.
11+
Only the spec half of that was ever built. **No renderer ever routed the member**:
12+
objectui's list-view switch shares its `default:` arm with `case 'grid'`, so a page
13+
view has always drawn an empty table where the page was supposed to be, and the
14+
three parse refusals that policed the binding policed a mount that never mounted
15+
anything. ADR-0049 enforce-or-remove; maintainer ruling 2026-09-09.
16+
17+
## FROM → TO
18+
19+
| you wrote (17.4 and earlier) | write instead |
20+
| --- | --- |
21+
| `{ type: 'page', pageName: 'sales_home', columns: [] }` on a list view | nothing on the view. Delete it, and reach the page from the app's `navigation`: `{ id: 'nav_sales_home', type: 'page', pageName: 'sales_home', label: 'Sales' }` |
22+
| `pageName` beside any other list-view `type` | delete the key — it was refused already, and is now a tombstone |
23+
| a list view that wanted rows | pick a row-drawing `type``grid` and its siblings, all unchanged |
24+
25+
**The one-line fix:** delete `type: 'page'` and `pageName` from the list view; put
26+
the page behind an app navigation item, which is a different key on a different
27+
surface (`PageNavItem.pageName`) and is the page mount that has always rendered.
28+
29+
`os migrate meta --from 17` lists the mechanical edits for existing sources; apply
30+
them by hand.
31+
32+
## The retirement kit
33+
34+
- **`pageName`** — a `retiredKey()` tombstone on `ListViewSchema` and
35+
`ObjectListViewSchema`. `tsc` types the key `never`, and a value reaching a parse
36+
raises the prescription rather than a bare unrecognized-key report.
37+
- **`'page'`** — an enum VALUE, so there is no tombstone to hang a prescription on
38+
(the def survives, one value lighter, and the four generated-surface ratchets are
39+
blind to that by construction). The `type` enum's own `error` map carries it,
40+
keyed on `issue.input` so only the value that used to be legal gets the
41+
"was removed" message; every other invalid `type` keeps zod's default text.
42+
- **`checkListViewPageMount`** — the exported object-level refinement existed only
43+
to police this mount, so it is removed with it, along with its three refusal
44+
messages. A downstream mirror that re-attached it (the reason it was exported)
45+
should drop the `.superRefine` line; the compiler delivers this one. It held no
46+
`ERROR_CODE_LEDGER` row — the three refusals were message constants, not codes.
47+
- **`validateViewPageRefs` / `VIEW_PAGE_UNRESOLVED`** (`@objectstack/lint`) — the
48+
`os validate` and publish-gate rule that resolved a mount against `stack.pages`.
49+
Removed: there is no reference left to resolve. Its nav twin
50+
(`validateNavTargetRefs`, on the app navigation item) is **untouched**.
51+
- **`RuntimeStackContext.pages`** (`@objectstack/lint`) and the `page` row of
52+
`CLOSURE_CONTEXT_KEY_BY_TYPE` (`@objectstack/metadata-protocol`) — the live page
53+
universe joined the per-write snapshot for that one rule, and leaves with it. A
54+
`PUT /api/v1/meta/view` publish no longer pays a `sys_metadata` round trip for a
55+
collection nothing consults. Hosts calling `runRuntimeAuthoringRules` /
56+
`evaluateRuntimeAuthoringGate` with an explicit `context.pages` drop that key.
57+
- **`defineStack`** — the `validateCrossReferences` branch that resolved a mount's
58+
`pageName` against `stack.pages` is gone. The surviving three page references in
59+
that function (an app nav item's `pageName`, a modal action's `target` at two
60+
rungs) keep their own policy.
61+
- **The metadata form**`view.form.ts`'s `page` section, whose one input was
62+
`pageName`, is removed. A form input for an unwritable key is the false-compliant
63+
UI half of a retirement.
64+
65+
## What an operator with a STORED page view sees
66+
67+
A `sys_metadata` `view` row written before this release can carry `type: 'page'` and
68+
a `pageName`. Nothing breaks at read: the ADR-0087 conversion
69+
`view-page-mount-removed` (protocol 18) replays on rehydration and strips both keys,
70+
so the row is served canonical. `type` is **stripped, not rewritten** — it defaults
71+
to `grid` in the schema, so the row lands on exactly what it already rendered
72+
without the platform guessing a view type.
73+
74+
The strip is announced once per row per process, on whichever seam served it.
75+
Grep for `carries a pre-protocol shape` — there are **three** emitters, one per
76+
rehydration seam, and they differ:
77+
78+
- `[DatabaseLoader] stored view/<name> carries a pre-protocol shape; <notice>`
79+
- `[ObjectQLPlugin] stored view/<name> carries a pre-protocol shape; <notice>`
80+
- `[Protocol] stored view/<name> carries a pre-protocol shape; <notice> The row
81+
itself is unchanged — re-save it (Studio edit -> save, or run
82+
"os migrate meta --stored --apply") to persist the canonical shape.`
83+
84+
`os migrate meta --from 17` lists the same edits for authored sources;
85+
`os migrate meta --stored --apply` rewrites the stored rows so the warn stops, and
86+
the next save through `PUT /api/v1/meta/view` heals one row the way it heals any
87+
pre-protocol shape.
88+
89+
⚠️ The conversion walks `stack.views[]` in all three persisted spellings; it does
90+
**not** reach `objects[].listViews.*`, which no conversion in the registry reaches.
91+
An object body still carrying a page mount is refused at its own door with the
92+
prescription rather than converted. Measured population for both at the ruling:
93+
**zero** authored `type: 'page'` list views in this repository or any consuming app
94+
the seats can read — the in-tree `type: 'page'` hits are all app nav items.
95+
96+
<!-- adr-0087: registered view-page-mount-removed -->
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/sdui-parser': patch
3+
---
4+
5+
`dashboard-widget-options.ts` header: `stageOrder` is a `funnel`-only key, not `funnel` / `pyramid`
6+
7+
The accepted-set census comment at the top of the module (carried into the
8+
published `index.d.ts`) described `stageOrder` as "funnel/pyramid stage order".
9+
There is no `pyramid` widget type: `ChartTypeSchema` refuses it, so an author
10+
who copied the pair got a parse refusal. The line now says what the schema's
11+
own `.describe()` says: `funnel` is the only widget type that reads the key.
12+
Comment-only — the accepted set, the diagnostic code and the emitted JS are
13+
unchanged.

.claude/agents/os-dev.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ model: opus
352352
- 两种读法通向两种架构时同此;⛔ 不写投机代码。
353353
- 返回 `status: "needs_decision"`,把每个问题连同选项、成本与你的推荐写进 `open_questions`
354354
- 升级分析的四轴决策框架由派发词携带,PM 从自己那份副本填入。
355-
- 已发布模板里它是 `rules/dev-template.md``{decision_frame}` 槽位
355+
- 框架唯一副本在 pm-dispatch SKILL.md 〈升级与决策〉;派发词逐字粘贴,dev ⛔ 不留副本
356356
- 每个方案逐轴分析,推荐也按那些轴给理由;派发词没带,停下向 PM 索取,⛔ 不自拟一套轴。
357357
- `main` 碎了、依赖未合并、CI 基础设施故障 ⇒ 报 `blocked` 附证据,重试到排除你的改动。
358358

.claude/hooks/guard-governed-enqueue.selftest.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ files_of() { # files_of path... -> the /pulls/{n}/files body shape
6060
printf '%s' "$out"
6161
}
6262

63+
# A RENAME is the one entry shape `files_of` cannot build: every other status
64+
# carries `filename` alone, a renamed one ALSO carries `previous_filename`.
65+
# Measured on PR #17372 (`GET /pulls/17372/files`): `filename` is the NEW path,
66+
# `previous_filename` the OLD one. `files_of` keeps its shape; this is the twin.
67+
renamed_of() { # renamed_of <old path> <new path> -> the /files body for one RENAME
68+
jq -nc --arg o "$1" --arg n "$2" '[{filename:$n,previous_filename:$o,status:"renamed"}]'
69+
}
70+
6371
approved_at() { # approved_at <login> <sha>
6472
jq -nc --arg l "$1" --arg c "$2" '[{state:"APPROVED",user:{login:$l},commit_id:$c}]'
6573
}
@@ -86,6 +94,8 @@ F_DISMISSED="$(fixture governed-dismissed "$GOVERNED_FILES" \
8694
F_CLEAR="$(fixture not-governed "$CLEAR_FILES" "$NO_REVIEWS")"
8795
F_REGEN="$(fixture pure-regeneration "$REGEN_FILES" "$NO_REVIEWS")"
8896
F_EMPTY="$(fixture empty-diff '[]' "$NO_REVIEWS")"
97+
F_RENAMED_OFF="$(fixture governed-renamed-off-the-surface "$(renamed_of AGENTS.md docs/AGENTS.md)" "$NO_REVIEWS")"
98+
F_RENAMED_CLEAR="$(fixture rename-within-an-ordinary-prefix "$(renamed_of packages/spec/src/a.ts packages/spec/src/b.ts)" "$NO_REVIEWS")"
8999

90100
mcp() { # mcp <tool> <pull> [owner] [repo]
91101
jq -nc --arg t "$1" --argjson n "$2" --arg o "${3:-objectstack-ai}" --arg r "${4:-objectstack}" \
@@ -201,6 +211,20 @@ echo "== nothing governed in the diff: allowed, and no review is ever consulted
201211
expect allow 'an ordinary diff enqueues freely' \
202212
"$(mcp $AUTO 14070)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_CLEAR"
203213

214+
echo "== a RENAME is a change to BOTH paths, so the OLD one is read too =="
215+
# Read `filename` alone and the old path is simply absent from the list handed to
216+
# the register — and a dropped path can only REMOVE governance, never add it, so
217+
# a diff that moves AGENTS.md to docs/AGENTS.md would read here as an ordinary
218+
# one. The other two readers of the same diff already see both paths (the queue
219+
# guard decomposes per commit with `--no-renames`, and `--pr` derives the list
220+
# three-dot), so this is the hook catching up to them, not a new predicate.
221+
expect block 'a governed file renamed OFF the governed surface is still governed' \
222+
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_RENAMED_OFF"
223+
expect_says 'AGENTS.md' 'the OLD path is the governed hit the refusal names' \
224+
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_RENAMED_OFF"
225+
expect allow 'a rename inside a non-governed prefix changes no verdict' \
226+
"$(mcp $AUTO 14070)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_RENAMED_CLEAR"
227+
204228
echo "== PURE REGENERATION: the hook must AGREE with the register, never re-decide =="
205229
# The requirement (maintainer 2026-09-01: 纯生成的指针行 … 不需要我审核吧) is that
206230
# this hook never re-closes a zero-approval path the register clears. Pinned

.claude/hooks/guard-governed-enqueue.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ try { d = JSON.parse(fs.readFileSync(process.env.OS_GUARD_FILE, "utf8")); }
400400
catch { process.exit(1); }
401401
const mode = process.env.OS_GUARD_MODE;
402402
if (mode === "head-sha") { const s = d && d.head && d.head.sha; if (!s) process.exit(1); console.log(s); }
403-
else if (mode === "filenames") { if (!Array.isArray(d)) process.exit(1); for (const f of d) if (f && f.filename) console.log(f.filename); }
403+
else if (mode === "filenames") { if (!Array.isArray(d)) process.exit(1); for (const f of d) { if (f && f.filename) console.log(f.filename); if (f && f.previous_filename && f.previous_filename !== f.filename) console.log(f.previous_filename); } }
404404
else if (mode === "count") { if (!Array.isArray(d)) process.exit(1); console.log(d.length); }
405405
else if (mode === "exceptions") console.log(((d || {}).exceptions || []).length);
406406
else if (mode === "hits") console.log((((d || {}).hitPaths) || []).join(", "));

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
244244
|:--|:--|
245245
| `domain:engine` | `packages/objectql``packages/core``packages/formula`(CEL / `matches-filter` / RLS 谓词求值)、`plugin-pinyin-search`;`packages/metadata*``packages/platform-objects`;`packages/drivers/driver-*`;退役标签 `domain:engine-core` / `domain:metadata` / `domain:drivers` 只退出流通,GitHub 标签对象保留 |
246246
| `domain:services` | `packages/services/*``packages/connectors/*``packages/triggers/*``plugin-approvals``plugin-webhooks``plugin-email``plugin-reports``embedder-openai``knowledge-*`;`plugin-auth``plugin-security``plugin-sharing``plugin-audit`;退役标签 `domain:identity` 同上只退流通 |
247-
| `domain:devx` | `sdui-parser``content/docs/**``apps/docs``.githooks/``docker/README.md``examples/**` 仅测试基建面;`packages/lint``scripts/`(门禁类)、`.github/workflows/`(接线)三者与 spec 相交面按锚定规则例外归 `domain:spec`,门禁按 SUBJECT:governed 面归 skills,代码/文档质量归本域 |
247+
| `domain:devx` | `sdui-parser``content/docs/**``apps/docs``docs/qa/**``.githooks/``docker/README.md``examples/**` 仅测试基建面;`packages/lint``scripts/`(门禁类)、`.github/workflows/`(接线)三者按锚定规则例外归 `domain:spec`,门禁按 SUBJECT:governed 面归 skills,代码/文档质量归本域 |
248248
| `domain:skills` | governed 面全量(含本文件;统一定义见「governed 面统一定义」行);非门禁的 `scripts/pm/**`(PM 循环工具);governed 面的治理执行文件:`.github/CODEOWNERS` + SUBJECT 是 governed 面本身的门禁/审计(现为 `scripts/pm/check-governed-merges.mjs`) |
249249
| `domain:spec` | `packages/spec` 整包:schema 形状、`contracts/**`、退役行为半边、strictness 台账;describe/JSDoc/墓碑散文/错误 guidance 与 alias 表;`packages/spec/scripts/**``packages/spec/docs/**` 及按锚定规则的例外归本域的工具链(域边界枚举与席内分派见 `references/lanes/spec.md`) |
250250
| `domain:cli` | `packages/cli``runtime``verify``packages/qa``types``packages/rest``packages/mcp``packages/observability``packages/client*``cloud-connection``create-objectstack``packages/adapters/*``plugin-hono-server``plugin-dev` |

.claude/skills/pm-dispatch/references/contract-review.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
- ③ 边界旗处置:dev 挂旗与 `open_questions` 逐旗答复或升级。
3333
- 独立性件,spec 席:契约判断真分叉或 dev 挂旗 ⇒ 起上下文隔离的达档复核子代理。
3434
- 只喂卡片、既有裁决与 PR 本体,⛔ 不喂派发令与派发席自己的结论;简报写成对抗性。
35-
- 裁决载独立性对(机读):产出 diff 的身份写 `Implemented-by:`,出裁决的席位写 `Reviewed-by:`
36-
- `mode:subagent` dev 记其分支 `claude/issue-…`(子代理无自有 session);`mode:remote` dev 记 session id
37-
- 两者同 session ⇒ 报 SELF-REVIEW,⛔ 不作独立复核;两行皆无的历史裁决恒静默。
35+
- 独立性对(机读):`Implemented-by:` 写产 diff 者身份;`mode:subagent` 记分支,`mode:remote` 记 session id
36+
- `Reviewed-by:` 写渲染或采纳裁决的席位 session;隔离复核子代理无 session,记采纳它的席位
37+
- 两者同 session ⇒ 报 SELF-REVIEW;值紧跟冒号,前置词即不可读;两行皆无的历史裁决恒静默。
3838
- 清标即落地:PASS ⇒ 同席同笔剥双载体;凡清标同笔留 provenance 评论,引记录 id 与所判 head。
3939
- 随后按 `landing-operations.md` 走落地前检 → 转 ready → 挂 auto-merge 或入队。
4040
- 轮次报告设复审清单专节,形状与代裁清单同为强制审计。

.claude/skills/pm-dispatch/references/lanes/engine.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
driver-sqlite-wasm、driver-turso。
1313
- 红线:改元数据格式或接受面 ⇒ `domain:spec`,判据是改变接受面而不是碰到 spec。
1414
- `/meta` 路由本体在 `packages/rest``domain:cli`;`packages/services/**``domain:services`
15-
- `content/docs/**``packages/lint``domain:devx`
15+
- `content/docs/**``packages/lint``domain:devx`;`packages/lint` 的唯一例外见 SKILL.md 锚定规则
1616

1717
## 常设承诺
1818

.claude/skills/pm-dispatch/references/platform-readings.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,11 @@
8181
- 入队事件与队列 ref 迟 1–3 分钟才出现 ⇒ 轮询预算按 3 分钟,⛔ 不按 1 分钟判没挂上。
8282
- 队列窗口有界:满窗条目 ref 与 `merge_group` run 双缺席,ref 随前一条落地才现,不按计时器。
8383
- ready 翻转触发检查重跑 ⇒ 入队落在翻转之后约一分钟,那段空窗不是挂载失败。
84+
- 检查全部完成的 PR 挂 auto-merge 即入队,本仓 28–60 秒 ⇒ 挂载与落地之间无窗口。
85+
- `mergeable_state` 未落定时挂上的是经典 auto-merge、不入队,落定后再挂才入队。
8486
- `behind` 的 PR 照常入队:落后于 main 不是入队否决,⛔ 不为它先跑 update-branch。
8587
- `check_suite.completed` 会命名过期 head,check-run 也只属最后一次 push ⇒ 用前先重读当前 head。
88+
- 落地相邻的写侧一则:GitHub 标签描述上限 100 字符,超长写回 422,133 字的原文即不可存。
8689

8790
## API 配额
8891

0 commit comments

Comments
 (0)