Skip to Content
變更紀錄

變更紀錄

自訂表格模組的變更紀錄,最新的發布排在最上面。每一筆都會標出它動到哪些端點,並連到教你怎麼用的頁面,讓你從「好像有東西變了」一步跳到「合約長這樣」。

怎麼讀一筆紀錄:

  • 破壞性:既有呼叫的結果會不一樣。每一筆都附上「你要改什麼」,那段就是完整的遷移步驟。
  • 新增:多了一個能力,你現在呼叫的東西不會改變行為。
  • 調整:既有呼叫被放寬或收緊,例如多一個欄位、多一個運算子、驗證變嚴格。
  • 修正:文件本來就這樣寫,但實作沒做到,現在修好了。

日期是測試環境(https://api.scfg.io)的發布日期。標示為已合併,尚未部署到測試環境的發布,表示它已經在 master 上、也已經寫進文件,但還沒部署,所以在部署之前那些端點仍是舊行為。正式環境走一般部署流程,所以請把日期讀成「可以開始接了」,而不是「每個租戶都已經生效」。API 參考頁與 OpenAPI 快照都跟下面每一筆同一批更新,所以只要路由出現在這裡,就一定已經在 API 參考。支援 user/public 驗證的路由也會成為可執行的 Playground 項目;特權 service 與 root 路由則會清楚標示為僅供參考。

類別
範圍
把這份紀錄當成一張自訂表格

下面每個欄位都是本模組真實存在的欄位型別:類別是 select、範圍是 select、端點清單是 text,發布日期是排序鍵。把這幾行貼進 IaC 工作台就能建出這張表。

{"kind":"table","ref":"changelog","spec":{"name":"Changelog","description":"Release log for the custom tables module"}}
{"kind":"column","table":"changelog","ref":"released_on","spec":{"type":"date","required":true}}
{"kind":"column","table":"changelog","ref":"change_kind","spec":{"type":"select","options":["breaking","new","changed","fixed"],"required":true}}
{"kind":"column","table":"changelog","ref":"area","spec":{"type":"select","options":["commands","column-types","querying","acl","iac","triggers","public-read","agent","ops"],"required":true}}
{"kind":"column","table":"changelog","ref":"title","spec":{"type":"string","max_length":160,"required":true}}
{"kind":"column","table":"changelog","ref":"detail","spec":{"type":"text"}}
{"kind":"column","table":"changelog","ref":"endpoints","spec":{"type":"json"}}
{"kind":"view","table":"changelog","ref":"breaking_only","spec":{"filter":{"change_kind":{"eq":"breaking"}},"sort":[{"field":"released_on","direction":"desc"}]}}

2026-11-04

已合併,尚未部署到測試環境
7 新增4 調整

效能專案的契約,於 2026-11-03 至 2026-11-04(Asia/Taipei)合併進 backend master:#2816(cursor 讀取資料列與資料列筆數)、#2820(Agent 工具箱、MCP 與 workspace SDK 採用同一套 cursor 與筆數契約)、#2806(指令摘要)、#2807(純量陣列指令 input 與精簡的 select)、#2805(bulk-insert ticket 耗時)與 #2808(回應壓縮);指令清單的 `?name=` 查詢來自 #2644。來源:teamsync-backend master 3856f7710f473602367ae2580b16047a4d2ffbc1(該 commit 的 VERSION 為 5.32.1),加上疊在它之上合併的 #2820(merge commit 8da4670c7a8a0b6ce281f963b8fb515a58eba49f,VERSION 同為 5.32.1),以及更正 #2824(已合併為 7eff5613e)。這裡沒有任何內容驗證過部署狀況。本次只涵蓋這些變更:5.22.0 到這些 commit 之間的其他變更,這裡尚未記錄。新的路由與有變動的 schema,放在一份小型的 OpenAPI overlay 中,疊在仍固定於 7c41cbc5(VERSION 5.13.0)的快照之上。請先檢查:沒有帶 `page_mode` 的請求行為和以前相同,仍會回傳精確的 `total`,但它現在已列為棄用,等前端遷移完成後,會連同 `offset` 與 `skip` 一起移除;資料列的分頁與筆數讀取(包含 offset 分頁)現在都有讀取時間上限,預設 15 秒,超過就回 `400 ct.query_timeout`;另外 `POST …/records/count` 的開放範圍跟著搜尋走,不對整合 API key 開放(和每一條路由一樣,舊版 `full` scope 的 key 是例外):#2816 起初讓 `third_party` 與 `specific_chatroom` key 也能呼叫它,#2824 把它重新對這類 key 關閉。

新增查詢

資料列的列表、搜尋與 saved view 讀取,可以改用有簽章的 cursor 分頁,不再做筆數統計(#2816)#

`GET …/records`、`POST …/records/search`、`GET` 與 `POST …/views/{view_id}/records`,以及整合檢視的 `GET …/view`,現在接受 `page_mode`(`offset`,預設值;或 `cursor`)與 `cursor`。第一頁送 `page_mode=cursor`,之後把每次回應的 `next_cursor` 當成 `cursor` 送回,直到 `has_more` 為 false。cursor 模式的回應帶有 `has_more` 與 `next_cursor`,`total` 與 `table.record_count` 則是 null,因為不做筆數統計;兩種模式都會回 `has_more`。cursor 是不透明、有簽章的字串,綁定查詢與呼叫者當下的權限:篩選、排序、檢視或權限一有變動,就回 `400 ct.invalid_cursor`,呼叫端要從第一頁重新開始;`503 ct.cursor_unavailable` 表示伺服器無法為 cursor 簽章。沒有帶 `page_mode` 的請求行為不變,仍會回傳精確的 `total`,它現在已標為棄用,等前端遷移完成後,會連同 `offset` 與 `skip` 一起移除。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/search
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/views/{view_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/views/{view_id}/records
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/view
新增查詢

`POST …/records/count` 在時間上限內回傳符合條件且可讀取的資料列精確筆數(#2816、#2824)#

body 與 `records/search` 相同,回應是 `{"count": <n>}`,依呼叫者的租戶、列 ACL、欄 ACL 與 SCP 規則計算。分頁與排序欄位可以帶,但會被忽略;搜尋沒有檢視 ID,所以要計算某個 saved view 的筆數,請送出它的篩選條件。這個筆數是精確值,並受讀取時間上限約束,所以請在要顯示數字的地方呼叫,不要每一頁都呼叫。筆數的開放範圍跟著搜尋走:它接受使用者的 JWT,整合 API key(`third_party`、`acquisition` 或 `specific_chatroom` key)則和搜尋、列表一樣被拒絕,回 `405`。#2816 起初讓 `third_party` 與 `specific_chatroom` key 也能呼叫這條路由,#2824 把它移除,所以只含 #2816、不含 #2824 的版本仍會接受這類 key;請不要依賴 key 來呼叫這條路由。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/count
新增複合指令

`GET …/command-summaries` 列出不含定義的指令;指令清單的 `?name=` 以完整名稱取得單一支(#2806、#2644)#

新的路由回傳和 `GET …/commands` 相同的指令:順序、`skip`、`limit`、`name` 與 `total` 都相同,但每一列是不含 `definition` 與 `interface` 的摘要:各種 ID、`name`、`description`、`agent_enabled`、`visibility`、`tag_id`、`execute_policy`、已儲存定義的 `version` 與 `mode`、`dependency_status`、兩個遮蔽旗標、`is_deleted`、`created_by` 與時間戳記。在後端的測試中,對一個有 114 支大型指令的範圍送出 `limit=200` 的請求,從清單路由回應約 17 MB,從摘要路由只有約 80 KB。選單與指令清單請用它;要取得單一支的定義,用清單路由的 `?name=<完整名稱>`,或 `GET …/commands/{command_id}`。`GET …/commands` 本身沒有改變,agent 與 MCP 的 `commands_list` 工具也沒有改變;和所有指令路由一樣,兩者都只接受 JWT。

  • GET /private/module/custom_tables/{scopeWithId}/command-summaries
  • GET /private/module/custom_tables/{scopeWithId}/commands
新增複合指令

指令的 input 可以是一串單純的值,`select` 可以用 `$in` 拿它篩選(#2807)#

`array` input 的 `item_schema` 現在可以是一個純量型別名稱(`string`、`integer`、`float`、`decimal`、`boolean`、`date` 或 `datetime`),而不是欄位對應表。項目不能是 null,陣列上限照舊(最多 200 個項目)。`select` 因此可以拿欄位去比對這串值,例如 `{"$in": ["$row.s.id", "$input.item_ids"]}`,可放在 `where` 或來源的 `filter`;step 就是這樣用一串 record id 選出資料列。在不是陣列的 input 上宣告純量 `item_schema`,儲存時會被拒絕,回 `422`。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
新增維運

bulk-insert 的 ticket 會回報送出與被領取的時間、執行了多久,以及每個 chunk 的耗時、鎖等待與 SQL(#2805)#

`bulk-insert` ticket 的 `GET /public/task/{ticket_id}` 現在帶有 `submitted_at`、`claimed_at`(排隊時間是 `claimed_at` 減去 `submitted_at`)、`elapsed_ms`(從被領取起算的 worker 時間)與 `chunk_metrics`;`chunk_metrics` 每個 500 列 chunk 一筆,有 `chunk`、`offset`、`rows`、`elapsed_ms`、`lock_wait_ms`、`sql_count` 與 `sql_ms`。`lock_wait_ms` 是取得資料表與上層資料表的鎖(guard)所花的時間,不是單純的資料庫鎖等待,而且和 `sql_ms` 有重疊。舊版伺服器寫的 ticket 沒有這些欄位,之後會隨 worker 寫入而出現;ticket 的其他部分沒有改變。

  • GET /public/task/{ticket_id}
新增AI 工具

MCP 的 `records_query` 接受 `page_mode` 與 `cursor`,新的 `records_count` 工具回傳精確筆數(#2820)#

MCP 伺服器 `teamsync-tables` 的 `records_query` 現在接受 `page_mode`(`offset`,預設值;或 `cursor`)與 `cursor`,單單帶 `cursor` 就會選用 cursor 模式。cursor 頁會讓 `total` 為 null,也不做筆數統計;預設的 offset 模式仍回傳精確的 `total`,它現在和 `offset` 一樣已列為棄用。新的讀取工具 `records_count` 接受 `table_id` 與 `filters`、`stored_filters`、`any_of`、`q`,在相同的讀取閘門與 ACL 規則下回傳 `{"count": n}`。公開的 MCP 伺服器仍是經過挑選、縮減的介面:兩個工具都沒有 `computed_filters` 參數。`sort_by` 現在也接受 `id`、`created_at` 與 `updated_at`。被拒絕的 cursor、超過時間上限的讀取,以及無法為 cursor 簽章的伺服器,分別回 `ct.invalid_cursor`、`ct.query_timeout` 與 `ct.cursor_unavailable`,錯誤訊息以該代碼開頭。

新增AI 工具

Workspace 資料通道可以讀 cursor 分頁,也能在需要時計數:`ct.query`、指令列與 Python SDK(#2820)#

`ct.query` 現在接受 `page_mode`(`offset`,預設值;或 `cursor`)、`cursor` 與 `operation`(`query`,預設值;或 `count`);`teamsync data ct query` 指令有 `--page-mode`、`--cursor` 與 `--operation`,shim 的 `teamsync_ct_query` 也有同樣三個欄位。結果多了 `has_more` 與 `next_cursor`;cursor 頁讓 `total` 為 null,預設的 offset 模式仍會計數,`operation: "count"` 則回傳 `{"count": n}`。被拒絕的 cursor 回資料通道的代碼 `invalid_cursor`,讀取超過時間上限回 `query_timeout`。在 Python SDK 中,`tables.query` 讀 cursor 頁(預設就是 cursor 模式),`tables.count` 負責計數;兩者都透過 `ct.call` 呼叫 MCP 工具 `records_query` 與 `records_count`,沿用同一套契約。

調整查詢

資料列的分頁與筆數讀取有時間上限,超過就回 `400 ct.query_timeout`(#2816)#

資料列的分頁與筆數查詢(包含 offset 分頁)現在都有讀取時間上限:預設 15 秒(部署設定 `CT_READ_QUERY_TIMEOUT_MS`,範圍 1 ms 到 60 秒)。讀取超過上限時,回 `400 {"detail": {"code": "ct.query_timeout", "limit_ms": 15000}}`,而不是慢慢跑完。請縮小篩選範圍,或改用 cursor 分頁。其他驗證錯誤維持原本的回應。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/search
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/count
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/views/{view_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/views/{view_id}/records
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/view
調整AI 工具

Agent 的 `custom_tables_query_records` 與 `custom_tables_find_records` 改用 `cursor` 分頁,不再回傳總數(#2820)#

`custom_tables_query_records` 與 `custom_tables_find_records` 現在接受 `cursor`,並回傳 `has_more` 與 `next_cursor`;結果不再有 `total` 或 `total_pages`,cursor 分頁也不做筆數統計。要接著讀,就用相同的篩選條件與排序再呼叫一次工具,把 `next_cursor` 當成 `cursor` 送回,直到 `has_more` 為 false;`custom_tables_query_records` 的 `page` 頁碼已列為棄用。`custom_tables_query_records` 另外接受 `operation: "count"`,對相同的 `filters`、`any_of`、`computed_filters` 與 `q` 只回傳 `{"count": n}`,分析 agent 被要求只在使用者問「有幾筆」時才去要。`absence` 現在跟著「空的第一頁」(沒有 `cursor`、`page` 為 1)出現,而不是跟著 `total` 為 0。cursor 綁定 principal、acting room、呼叫者當下的權限、資料表欄位結構、查詢與排序:對不上的 cursor 回 `{"error": "ct.invalid_cursor", "code": "ct.invalid_cursor"}`,讀取超過時間上限則回 `ct.query_timeout`。依 `sort_order`、`id`、`created_at`、`updated_at` 以外的欄位排序時,cursor 內部以略過筆數接續,所以越後面的頁面成本越高。

調整複合指令

指令的 select 只讀它引用到的欄位,用 id 篩選也不再掃過整張資料表(#2807)#

引擎現在會先編譯 step 的 join、條件、投影、分組、having 與排序,再建立來源,而且只讀它們引用到的欄位;step 只投影的 link 欄位,是針對篩選後留下來的資料列才去取。用 `id`,或用 `$in` 給一串 id 篩選來源(`$in` 是整個 `where` 或 `filter`,或是最上層 `$and` 的其中一項),成本不再取決於資料表大小;用其他欄位篩選,或把 `$in` 放在 `$or`、`$not` 底下,成本仍會隨資料表變大。後端拿它的測試資料比對了變更前後的輸出,結果相同,所以只有指令所需的工作量不同。想讓檢查保持便宜,請讀由指令維護的儲存餘額,不要在檢查時才把流水加總。

調整維運

呼叫端送出 `Accept-Encoding: gzip` 時,大型 JSON 回應會被 gzip 壓縮(#2808)#

請求帶有 `Accept-Encoding: gzip` 時,大約 1 KB 以上的 JSON 回應會被壓縮;瀏覽器會自動送出這個 header,也會自動解壓縮。解壓縮後是同一份 JSON,回應帶有 `Content-Encoding: gzip` 與 `Vary: Accept-Encoding`。在後端的測試中,約 2.4 MB 的一頁資料列,傳輸量只有約 0.5 MB。串流、匯出檔(CSV、XLSX)、下載與其他非 JSON 的回應不會壓縮;沒有送這個 header 的呼叫端,仍和以前一樣收到未壓縮的內容。以 `JSONResponse` 產生的回應,以及沒有宣告回應模型的路由,現在改用 orjson 編碼;有宣告回應模型的路由(資料列與指令路由)沿用原本的序列化方式。解碼後的值相同,差別只有兩點:NaN 與無限大現在輸出 `null`(以前是 `500`),指數可能印成 `1e16` 而不是 `1e+16`。

2026-10-09

5.22.0已合併,尚未部署到測試環境
1 破壞性1 新增4 調整5 修正

Backend 5.22.0,於 10 月 9 日打上 tag v5.22.0。來源 pin:teamsync-backend 36270f5a2b83fdd71b6641ad94cee707f8028292(即 tag v5.22.0 的程式碼樹;tag 所在的 commit ceee9a325d17d08a33e6c5bd94b9ecc3d1c84112 只把 VERSION 改為 5.22.0),與 5.21.0(172a7f80bdf4dbdffdc018eb08bfa42c62bb485c)比較。本次僅更新 Custom Tables,涵蓋該範圍內 9 個與 CT 有關的 commit;範圍內另外 14 個 commit 是 Edge Workers、Sandbox 與基礎設施的變更,本站不涵蓋;只有測試與伺服器內部的 commit 不單獨成條,列在 CT kernel 補齊頁。請先檢查:plan 建立、更新、刪除或搬移超過 300 個資源的 IaC apply 回 202 並附 ticket,所以只把 200 當成功的 client 會出問題;含座標欄位資料表的既有 IaC plan hash、Command 提案與暫存世代會失效一次,座標索引狀態也不再是資料表 settings 的一部分(在 5.21.0 上傳、文件帶有它的 package input 必須重新上傳);公開讀取的 query 最多接受 3 個 sort 鍵,超過回 400;definer 權限 Command 的 dry run,對 execute-only 呼叫者的錯誤會像執行一樣被遮蔽。新功能:作者撰寫的 Command 可在明確的 grant(attachment_copies)下,讓原本無法自行複製的呼叫者(例如外部 client)跨資料表複製指定附件。可靠性:鎖衝突不再讓背景新增欄位永久失敗;apply 準時回傳的 IaC ticket 以 finished 結束;單一 API worker 收到大量已驗證請求時,請求也不再互相卡住。沒有查詢 staging 或 production 端點,也不宣稱已部署、瀏覽器驗收或新增產品測試。離線端點目錄、OpenAPI 快照與 Playground 仍固定在各自較早的版本(7c41cbc5,VERSION 5.13.0),所以新的 request 欄位只在概念頁說明,不是可執行的目錄項目。

破壞性IaC

IaC apply 將變更超過 300 個資源的 plan 改走 ticketed 非同步 apply(#2167)#

只要 plan 建立、更新、刪除或搬移的資源超過 300 個(CUSTOM_TABLE_IAC_SYNC_CHANGE_LIMIT;noop、adopt、orphaned 與 record action 不計入,剛好 300 個不會觸發這條規則),apply 現在會對整份文件回 202 與 ticket,而不是在 request 內套用後回 200。這條規則不會因為重新套用未變更的文件,或只對大文件做小幅修改,就改走 ticket(record-line 與重量級欄位變更規則仍各自適用)。除非 legacy 是部署預設值,否則對這類 plan 明確指定 worker=legacy 會被 400 ct.iac_legacy_backfill_unavailable 拒絕,並帶有它自己的訊息;legacy 為預設值時,ticket 會等待 IaC slot,並可能以 failed 與 ct.iac_legacy_backfill_busy 結束。

你要改什麼apply 的兩種回應都要處理:`200` 表示文件已在 request 內套用完畢,`202` 表示文件已被接受為 ticket,request 內沒有執行任何東西。收到 `202` 時,以送出者的身分輪詢 `GET /public/task/{ticket_id}`,直到 `body.status` 為 `finished`(像同步回應一樣讀取 `body.result`)或 `failed`,再重新 plan 確認已收斂。只把 `200` 當成功、或假設 request 回傳時文件已套用的 client,遇到變更超過 300 個資源(預設上限)的 plan 就會出問題。除非 legacy 是你部署的預設值,否則請從這類文件中移除明確指定的 `worker=legacy`:它會被 `400 ct.iac_legacy_backfill_unavailable`(帶有它自己的訊息)拒絕,不論文件有多少條 record line。
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
  • GET /public/task/{ticket_id}
新增複合指令

Command 可在作者宣告的 grant 下,為任何呼叫者(包含外部呼叫者)跨資料表複製指定附件(#2159)#

version 2 的寫入型 Command 可以帶 `attachment_copies` grant(最多 8 個,每個寫入 step 的每個目的欄位一個):每個 grant 指名一個 `select` step 與它的附件欄位、以 `$single` 或 `$first` 接收檔案的 `insert` 或 `update` step,以及 `exposure: "destination_row_readers"`,意思是任何讀得到目的資料列與欄位的人(包含呼叫者本人)都讀得到每個被複製的檔案。grant 需要 invoker authority,所以呼叫者仍需要來源資料表的讀取權,而且只能透過 Commands REST API 儲存(IaC 與 Composer 會拒絕);儲存該版本的使用者在每次複製時都會被重新檢查,每次複製都會記錄在 execution 的 `authorization_snapshot.attachment_copies`,部署也可以用 `CUSTOM_TABLE_COMMAND_ATTACHMENT_GRANTS=off` 拒絕所有 grant。沒有 grant 或 grant 檢查失敗時,外部 client 對另一張資料表持有的檔案仍會得到 `Unknown or inaccessible attachment ids`。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/validate
  • GET /private/module/custom_tables/{scopeWithId}/commands/{command_id}/executions/{execution_id}
調整複合指令

definer 權限 Command 的 dry run,對 execute-only 呼叫者的錯誤會和執行一樣被遮蔽(#2166)#

呼叫者若未管理 `authority: definition` 的 Command 所引用的每一張資料表,對它做 dry run(`dry_run=true`)現在會得到與執行相同的狀態碼和 body,即 `{"detail": {"error": "command_failed", "message": "The command could not be completed.", "redacted": true}}`,會原樣通過的錯誤碼與公開的 `assert` 訊息也相同。以前它會回完整的引擎錯誤:錯誤碼、`phase`、`step`、`path`、`details`、assertion 訊息與輸入驗證文字。對不存在的 Command 做 dry run,也改回這個被遮蔽的 `404`,而不是 `404 Command not found`;管理每一張被引用資料表的呼叫者(作者通常就是)與 invoker-authority Command 的呼叫者仍看得到完整 detail。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
調整查詢

座標索引狀態由伺服器擁有:任何資料表讀取與 IaC export 都不再回傳,帶它的建立請求會被拒絕(#2166)#

資料表讀取、建立回應、scope 清單、MCP 的資料表讀取與 IaC export 不再帶有 `settings._coordinate_cells_v1`;仍待處理的索引,只會從座標查詢較低的候選上限看出來。REST 建立與 bootstrap 收到含這個 key 的 `settings` 時回 `422`(`Coordinate index state is server-owned`),所以請不要把舊版回應中的 `settings` 原樣複製進建立 body。帶有這個 key 的 IaC 文件(例如 5.21.0 匯出的)在 plan 與 apply 仍會被接受,這兩者會丟掉這個 key,也不會儲存、比對或在回應中回傳它。5.22.0 之前上傳、table 行帶有這個 key 的 package input 是例外:規劃或套用引用它的 package 會回 `422` `Input semantic commitment does not match its bytes`,請以新的 `request_idempotency_key` 重新上傳該文件(沿用舊的 key 只會重播已儲存的 input,其 digest 仍是舊值),並在新的 manifest 中引用新的 input(若是 `resource` input,還要把新 input 的 semantic digest 宣告為它的 `resource_digest`)。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}
  • POST /private/module/custom_tables/{scope}/tables
  • GET /private/module/custom_tables/{scope}/tables/iac/export
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
  • POST /private/module/custom_tables/company/{company_id}/packages/plan
調整維運

含座標欄位資料表的 plan hash、Command 提案與暫存世代,會在升級到 5.22.0 時失效一次(#2166)#

IaC 的 plan 指紋、Command 提案的 policy snapshot 與暫存世代的定義 digest,都不再包含座標索引狀態。升級前就為含座標欄位的資料表凍結的 `plan_hash`(apply 回 `409 plan_stale`)、提案(準備者檢視、每一次正式寫入、重送同一個 request key,以及讀取由它送出的 operation,都回 `409 ct.proposal_stale`)或世代(啟用時回 `409 ct.definition_stale`,且該世代終止為 `failed`),會在升級時失效一次(沒有索引狀態的資料表,digest 不變):請重新 plan、重新準備或建立新的世代,有帶 key 的請求要換新 key。之後索引進度不會再讓提案或世代失效;這類資料表上尚未套用的 IaC plan 在其索引回填期間仍會失效,因為它的指紋還涵蓋資料表的 `updated_at`。

  • POST /private/module/custom_tables/{scope}/tables/iac/apply
  • GET /private/module/custom_tables/{scopeWithId}/proposals/{proposal_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/operations
  • POST /private/module/custom_tables/company/{scope_id}/generations/{generation_id}/activate
  • POST /private/module/custom_tables/department/{scope_id}/generations/{generation_id}/activate
調整權限

被隱藏的欄位在以紀錄欄位排序的 saved view、MCP records_aggregate 與 agent 計算篩選中,回應與不存在的欄位相同(#2151)#

顯示名稱為 `created_at`、`updated_at`、`created_by` 或 `created_by_client` 的隱藏欄位,不再在 saved view(REST apply 與 MCP `views_apply`)或 MCP `records_query` 的舊式 `filters` 中佔用該名稱:以 `created_at` 排序的 view 改為依紀錄欄位排序,不再落回 record id 順序,這些 filter 也能再次命中。MCP `records_aggregate` 對 `group_by` 中被隱藏的顯示名稱回 `not_found`,與不存在的名稱相同(以前回 `invalid_argument`,並帶出隱藏欄位的內部 key);agent 工具 `custom_tables_query_records` 對 `computed_filters` 中被隱藏的名稱回 `Column '<name>' does not exist in table schema`,與不存在的名稱相同(以前是 `Column '<name>' not found.`)。嚴格的讀取(list、search、export)與多欄位 `sort` key 維持不變。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/views/{view_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/views/{view_id}/records
修正IaC

準時回傳的 IaC apply,其 ticket 以 finished 結束,而不是 ct.iac_apply_timeout(#2151)#

apply 若在 attempt 期限之前就已回傳,它的 finished 結果現在會取代搶先寫入的 ct.iac_apply_timeout(worker 的 heartbeat、某次 status poll 或重新投遞的工作寫的皆然),所以之後的 poll 可能讀到 finished。超過期限才回傳的 apply 則維持 timeout。先前結果只會取代 heartbeat 寫的 timeout(即使回傳已晚),從不取代 status poll 或重新投遞的工作寫的 timeout。出現 timeout 之後,請重新 plan 確認實際套用了什麼。

  • GET /public/task/{ticket_id}
修正公開讀取

公開讀取 query 的 sort 超過 3 個鍵時,改回 400 而不是 500(#2166)#

公開讀取的 query body 以前允許最多 10 個 `sort` 鍵,但引擎的排序上限是 3,所以 4 到 10 個鍵會以 `500 Internal Server Error` 結束。現在 body 最多允許 3 個:更長的清單回 `400`,其 `detail` 以 `invalid body:` 開頭並指出 `sort`,與該通道其他 body 驗證錯誤一致。送 1 到 3 個鍵的 client 不受影響;以前送 4 到 10 個鍵的 client 會得到 `500`(超過 10 個鍵本來就是 `400`),需要縮短清單。私有的資料列搜尋對第 4 個鍵回 `422`。

  • POST /public/module/custom_tables/read/{token_id}/query
修正欄位型別

死結或鎖等待逾時不再讓背景新增欄位永久失敗(#2155)#

回填語句遇到死結(MySQL 1213)時,會在 worker 內重跑,最多共三次。鎖等待逾時(1205),或重跑後仍發生的死結,會把該次嘗試結束為 `ct.column_add_interrupted`,下一次經授權的狀態輪詢會接續工作,只填補仍缺的 cell(期間寫入的值,包含明確的 `null`,都會保留);建立座標索引或把工作標記為 completed 時發生的鎖衝突也以同樣方式處理。第三次因鎖衝突而結束的嘗試會把工作結束為 `ct.column_add_failed`;以前第一次就會如此,且沒有任何輪詢會重試。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/migration/status
  • GET /private/module/custom_tables/{scopeName}/migrations/{migration_id}/status
修正複合指令

不含 child_collection step 的 Command 在 /execute 時,核准規則的來源資料表不在 tag 內,改回 422 command_tables_outside_tag,而不是 500(#2151)#

對不含 `child_collection` step 的 Command 呼叫 `POST .../commands/{command_id}/execute`,若它的某次寫入觸發的核准規則,其 Review 表單輸入的來源資料表不在該 Command 的 tag 內,現在回 `422`,內容為 `command_tables_outside_tag`、`tag_id` 與 `table_ids`(來源資料表),且不寫入任何東西;以前回的是通用的 `500 command_execution_failed`。對 `authority: definition` 的 Command,只有 execute 權限、且不管理每一張被引用資料表的呼叫者,拿到的狀態碼仍相同,但內容是經過遮蔽的 `command_failed`。呼叫者可以對某個 mapped 子表 insert、卻不能讀取它時,child-collection bundle 的 prepare 現在以 `403 ct.permission_denied`(`do_not_retry`)拒絕;以前它會簽發一份 proposal,其 operation 一律回 `409 ct.proposal_stale`。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/prepare
修正維運

單一 API worker 收到大量已驗證請求時,請求不再互相卡住(#2158)#

當一個 worker 同時處理的已驗證請求數量接近資料庫連線數時,這些請求可能互相等待直到資料庫連線池逾時,仍在等第一條連線的請求隨後會回 `503 Database connection temporarily unavailable. Please try again in a moment.`。現在 request log 改在回應之後寫入,請求取得第一條資料庫連線時也使用自己的限流器,所以這類突發流量能順利處理完。請求與回應的形狀都沒有改變。

2026-10-08

5.21.0已合併,尚未部署到測試環境
4 破壞性8 新增12 調整18 修正

Backend 5.21.0,於 10 月 8 日打上 tag v5.21.0。來源 pin:teamsync-backend 172a7f80bdf4dbdffdc018eb08bfa42c62bb485c(VERSION 5.21.0),與 5.20.1(4a6921b7e2b1b09f9c9cd5615210e3cc234fe364)比較。本次僅更新 Custom Tables,涵蓋該範圍內 45 個與 CT 有關的 commit;只有測試與伺服器內部的 commit 不單獨成條,列在 CT kernel 補齊頁。請先檢查:其他公司的 id 在原本部分路由回 403 之處改為回 404;大型 IaC 欄位異動回 202 並附 ticket 供輪詢;超過 500 筆 live 紀錄的 REST 欄位新增改在背景執行;CSV 匯出會標記看起來像公式的文字,匯出的大小與產生時間也有上限。可靠性:Custom Tables 的資料庫工作改在 event loop 之外執行,full-GC 暫停不再卡住 worker,達到 max-requests 上限的 worker 會先完成處理中的背景工作再回收(預設最多 15 分鐘)。沒有查詢 staging 或 production 端點,也不宣稱已部署、瀏覽器驗收或新增產品測試。離線端點目錄、OpenAPI 快照與 Playground 仍固定在各自較早的版本(7c41cbc5,VERSION 5.13.0),所以新的 request 欄位只在概念頁說明,不是可執行的目錄項目。

破壞性權限

其他公司的資料表、房間、link 目標、診斷與 trigger 目標 id,現在的回應與不存在相同(#2101、#2110、#2112)#

其他公司的 id 不再洩漏它們是否存在。資料表 id 路由上的資料表 id 現在回 `404 {"detail": "CustomTable Not found"}`(匯入 v2 維持 `Table not found`,子集合與 bundle 維持 `Custom table not found`),聊天室 id 回 `404 Chatroom Not found`(在「於房間之下查找資料表」的路由上,外來或不存在的房間 id 改回該路由自己的資料表 404),link 的 `target_table_id` 回 `400 Link target table not found`,診斷的 `migration_id` 回 `404 Diagnosis results not found`,trigger action 的目標回 `400 target table not found or not accessible`。這些情況以前回 `403 Insufficient permissions. (Different Company)`,或回一個能分辨兩種情況的錯誤。例外是個別使用者的 `permissions` 授權路由:不是管理者、id 不存在、其他公司的資料表,一律回同一個 `403 Only table managers (creator, scope manager, or table moderator) can manage permissions.`。有一項變更也影響你自己的公司:先檢查房間的聊天室路由上,不存在或已刪除的 `chatroom_id` 現在會在任何資料表門檻執行之前回 `404 Chatroom Not found`,即使過去只靠資料表門檻就會放行也一樣。其他同公司的 403 與 metadata 讀取的隱藏資料表 404 不變。

你要改什麼把資料表、房間、link 目標、診斷或 trigger 目標 id 的 404 視為「對你而言不存在」:不要重試,並移除把 `403 Insufficient permissions. (Different Company)` 當作「該 id 屬於其他公司」證據的分支。同公司的權限不足與個別使用者 `permissions` 授權路由的 403 仍要照常處理。在先檢查房間的聊天室路由上,路徑中不存在或已刪除的聊天室 id,現在會在查詢資料表之前就回 `404 Chatroom Not found`,即使資料表屬於你自己的公司也一樣。
  • GET /private/module/custom_tables/{scope}/tables/{table_id}
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/columns
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
  • GET /private/module/custom_tables/{scopeName}/migrations/{migration_id}/diagnosis-results
破壞性IaC

IaC apply 將大型表格上的重量級欄位變更改走 ticketed 非同步 apply(#2091、#2111、#2118)#

欄位新增、刪除或會改寫資料的更新,只要碰到 live 記錄超過 10,000 筆的既有表格(刪除時已進垃圾桶的記錄也計入),apply 現在會對整份文件回 202 與 ticket,而不是在 request 內執行;請以送出者的身分輪詢 ticket。除非 legacy 是部署預設值,否則對這類文件明確指定 worker=legacy 會被 400 ct.iac_legacy_backfill_unavailable 拒絕;拿不到 IaC slot 的 legacy 回填,ticket 會以 failed 與 ct.iac_legacy_backfill_busy 結束。

你要改什麼照原樣送出 apply,但把 `202` 視為「已派送」而不是「已套用」:以送出者的身分輪詢 `GET /public/task/{ticket_id}`,直到 `body.status` 為 `finished`(像同步回應一樣讀取 `body.result`)或 `failed`,再重新 plan 確認已收斂。不要假設會改動大型資料表欄位的文件,在請求回傳時就已經套用。除非 legacy 是你部署的預設值,否則請從這類文件中移除明確指定的 `worker=legacy`:它會被 `400 ct.iac_legacy_backfill_unavailable` 拒絕。
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
  • GET /public/task/{ticket_id}
破壞性欄位型別

REST 欄位新增在超過 500 筆 live 紀錄時改為背景回填並回傳 migration id(#2105、#2116、#2135)#

經由 REST 欄位路由對超過 500 筆 live 紀錄(原為 10,000)的表新增一般欄位時,schema 會立即 commit,並回 migration_info.background_migration 為 true 與 migration_id。回填結束前紀錄寫入會回 423,寫入前請輪詢 migration id 狀態或 table migration status。IaC 的 column 行與 Agent 工具箱的 columns_add 不受影響,一律同步新增。

你要改什麼呼叫 POST .../columns 後讀取 migration_info。background_migration 為 true 時,輪詢 GET .../migrations/{migration_id}/status 直到 status 為 completed,或輪詢 table migration status 直到 migration_status 為 completed,之後再寫入紀錄;光是 is_locked 為 false 不代表新增已完成。仍是 queued 或 processing 的新增請持續輪詢,因為只有輪詢會重新啟動被中斷的新增(error_code 不是 ct.column_add_failed 者,下一次輪詢都會重試);status 為 failed 且 error_code 為 ct.column_add_failed 是最終狀態。
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/columns
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/migration/status
  • GET /private/module/custom_tables/{scopeName}/migrations/{migration_id}/status
破壞性查詢

CSV 紀錄匯出會標記試算表可能當成公式執行的文字(#2115)#

以 `=`、`+`、`-` 或 `@`(或全形形式,前面可先有空白)、撇號或控制字元開頭的 CSV 標頭或文字 cell,現在會在前面加上撇號與零寬空白(U+0027 U+200B)並加上引號;真正的數字,以及數字型別 cell 中的純數字字面值,維持原本的位元組。讀取匯出檔的程式應去掉這個兩字元前綴。CSV 檔案匯入與 MCP 的 `records_export` 檔案遵守同一份約定,所以匯出檔再匯入可原樣還原。

你要改什麼讀取 CSV 的程式,遇到以這個兩字元前綴(撇號後接 U+200B)開頭的標頭或文字 cell 時,必須先把它去掉。本模組的 CSV 檔案匯入已會處理,所以匯出檔再匯入可原樣還原。XLSX 不需要修改。
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/export
新增查詢

座標半徑篩選、連結原點篩選與距離排序(#2072)#

紀錄 search、export、saved view 與 public-read 查詢接受 stored_filters 的 within 運算子與 sort[].distance_from,已登入的呼叫者還可用 within_any(public-read 呼叫者用它會得到 400 ct.coordinate_origin_read_authority_required,除非請求先被 ct.coordinate_origins_require_link、ct.coordinate_reference_untrusted 或 ct.coordinate_indeterminate 拒絕),並由持久化 cell 索引支援。已儲存篩選絕不把未知當成範圍外:座標為空、缺少原點或擷取證據未通過時,整個請求以 400 ct.coordinate_indeterminate 與 reason 拒絕。候選上限為 20,000(已索引)與 50,000(索引待處理),執行時間 5 秒。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/search
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/export
新增維運

較大資料表上的座標索引待處理狀態與 operator 回填(#2072、#2116)#

在預設設定下,對儲存紀錄(含垃圾桶中的)超過 500 筆的表新增座標欄,索引會維持待處理;唯一例外是 501 到 10,000 筆 live 紀錄的 REST 新增,它會以背景遷移建立索引。因此每一個 IaC column 行、每一次 Agent 工具箱的 columns_add(兩者一律同步新增),以及每一個超過 10,000 筆 live 紀錄的 REST 新增,都會讓索引待處理。變更座標欄的 precision、scale 或 bounds,或還原垃圾桶中的表,在超過 500 筆儲存紀錄時同樣讓索引待處理。查詢仍能精確回答,但最多只評估 50,000 個候選,超過則回 ct.coordinate_query_cap。Operator 用可續跑的回填補完索引(說明見座標查詢頁;需要 JSON body,用預設值就送 {}),重複呼叫直到 complete 為 true。

新增複合指令

Command 運算式可把時間點轉成當地時間與日期,並以天數位移日期(#2060)#

版本 2 的 Command 運算式新增 `$time_of_day`、`$local_date`、`$date_add` 與嚴格的 `{"$time": "HH:MM"}` 常值。請在第二個運算元明確指定 IANA 時區:無效的常值在儲存時被拒絕,執行期無效的時區回 `400 invalid_expression` 且不會退回其他時區;不指定時區則使用公司業務時區(未設定時為 UTC)。`time` 值只能與另一個 `time` 比較;`select` 的 `where` 針對資料列 `datetime` 欄位時,會在 `limit` 與 `max_rows` 之前過濾,並受每個 select 20 000 列、每次執行 50 000 列的工作列上限約束。

新增複合指令

Command 的 insert step 可用 on_conflict 針對 unique_tuple 規則做 upsert(#2066)#

版本 2 的 `insert` step 可帶 `on_conflict: {rule, action: "update" | "nothing" | "error", set}`,指名一條已啟用的 `unique_tuple` 規則;`set` 以 `$existing.<column>` 讀取既有的那一列。該 step 的 `returning` relation 一定帶 `outcome`(`inserted`、`updated` 或 `unchanged`)、`record_id` 與 `version`,呼叫者讀不到結果列時 relation 為空。動作為 `update` 或 `nothing` 時,呼叫者讀不到的衝突列回 `409 unique_tuple_conflict`,body 只有 `error` 與 `message`(沒有 `constraint` 或 `columns`);而待審 hold 會凍結暫存當下選定的分支。stored formula 欄位可以作為鍵的 component。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
新增複合指令

Command execute 支援 dry_run=true 預覽版本 2 的執行(#2066)#

`POST .../commands/{command_id}/execute?dry_run=true` 對版本 2 的寫入型 Command 回 `200 {"status": "preview", "outputs": {...}}`。它執行同樣的驗證、權限與規則檢查後整個回滾,所以不持久化任何東西:沒有資料列、history、待審項目、trigger、callback 或 idempotency 保留。預覽只存在於有 scope 的 execute 路由:agent lane 的 execute 路由會忽略 `dry_run`,並真的執行該 Command。版本 1 的 Command 回 `400 Preview requires a version-2 Command.`。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
新增複合指令

作者選擇公開的 Command 結果與業務事實聊天 receipt(#2083)#

在 version-2 的純量 output 或 `assert` step 設定 `"public": true`,definer(`authority: definition`)Command 的 execute-only 呼叫者就會在 `outputs` 與 `public_receipt` 收到這些值;失敗的公開 `assert` 回 `422 assertion_failed` 並附作者寫的 message,而不是 `command_failed`。除非每個來源都能證明公開,否則撰寫階段會以 `command_public_value_unsafe` 或 `command_public_value_limit_exceeded` 拒絕;每次交付也會用目前的欄位限制重新檢查,不再符合的項目會被扣留。在聊天中,已套用 command 的 receipt 會改為陳述 Command 名稱、輸入與公開 output,而不是固定句。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/validate
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • GET /private/module/custom_tables/{scopeWithId}/commands/{command_id}/executions/{execution_id}
新增維運

工作日行事曆的唯讀 API key scope(#2050)#

scope 為 work_calendar_read 的 API key(以 POST /private/auth/apikey 簽發)只能讀取其擁有者公司的 GET …/core/work-calendars 與 …/work-calendars/{key},其他一律不行:PUT 回 403 Access denied, invalid scope,其他多數路由(例如能力探索)回 405,input、artifact 與 package 路由則回 403。third_party、specific_chatroom 與舊版 full key 在兩條 GET 路由都回 403,讀取不需要管理者角色。回應含 actor_user_id 與 business_timezone,因此只應把此 scope 授予可以看到這兩者的整合。

  • GET /private/module/custom_tables/company/{company_id}/core/work-calendars
  • GET /private/module/custom_tables/company/{company_id}/core/work-calendars/{key}
新增維運

工作日行事曆例外的選用 label(#2050)#

例外可帶 label:最多 64 個 Unicode 字元的純文字備註,原樣儲存,為 null 時讀取會省略。允許 tab 與換行;U+0000–U+0008、U+000B–U+001F 與 U+007F 以一般的清單形 422 拒絕。REST、IaC work_calendar line、MCP work_calendar_get 工具與 Agent 唯讀工具都會帶出;WORKDAY、NETWORKDAYS 與 ISWORKDAY 忽略它,只改 label 的 PUT 仍會儲存新版本。

  • PUT /private/module/custom_tables/company/{company_id}/core/work-calendars/{key}
調整IaC

IaC request 在伺服器時間預算內執行(#2048)#

Plan、state、export 與 apply 的唯讀階段超過伺服器預算(預設 120 秒,含等待 IaC slot 的時間)時,會回 422 與 code ct.iac_time_budget_exceeded,而且不寫入任何東西。請稍後重試或改送較小的文件。同步 apply 一旦開始執行各行,預算就不再適用。

  • POST /private/module/custom_tables/{scope}/tables/iac/plan
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
  • GET /private/module/custom_tables/{scope}/tables/iac/state
  • GET /private/module/custom_tables/{scope}/tables/iac/export
調整維運

欄位新增、更新與刪除依 API worker 准入並回具型別的 429(#2105、#2116)#

一個 worker 預設同時最多執行四個欄位操作。再來的請求會立即被拒絕,不會排隊,回 429、Retry-After: 1 與 detail.code ct.column_operation_busy 及 concurrency_limit。請在延遲後重試;背景新增在回填結束前會一直佔用名額。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/columns
  • PATCH /private/module/custom_tables/{scope}/tables/{table_id}/columns/{column_id}
  • DELETE /private/module/custom_tables/{scope}/tables/{table_id}/columns/{column_id}
調整維運

背景新增欄位有狀態紀錄、具型別錯誤與由輪詢驅動的復原(#2135)#

新增工作的狀態從一開始就存在(queued、processing、completed 或 failed),table migration status 也帶 migration_id 與 migration_status。被中斷的新增會在表鎖失效後由經授權的輪詢重新啟動(ct.column_add_interrupted 或 ct.column_add_timeout,只填補缺少的 cell);ct.column_add_failed 是最終狀態。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/migration/status
  • GET /private/module/custom_tables/{scopeName}/migrations/{migration_id}/status
調整維運

新增欄位、刪除欄位或清除資料表時遇到 Redis 中斷,現在回有型別的 503,而不是帶驅動程式文字的 500(#2117)#

Redis client 在新增欄位、在最多 10,000 筆有效資料列的資料表上刪除欄位,或執行單一資料表的清除(此時一次性的清除 ticket 已經用掉,重送同一個請求會回 `404 purge_ticket_not_found`)時失敗,回應現在是 `503 {"detail": {"error": "dependency_unavailable", "message": "Operation temporarily unavailable; retry."}}`,不含驅動程式文字,也沒有 `Retry-After` 標頭;過去是 `500`,`detail` 為原始的 client 例外文字。請用自己的退避重試,並先重新讀取資料表的欄位。其他路由對非預期例外仍維持各自原本的回應。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/columns
  • DELETE /private/module/custom_tables/{scope}/tables/{table_id}/columns/{column_id}
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/purge
調整IaC

IaC apply ticket 使用私有的 version-8 id,並以 typed 結果結束(#2111、#2113、#2117、#2118)#

新的 IaC apply ticket 是 version-8 UUID,和以前一樣只有送出 apply 的使用者能輪詢;其他呼叫者與任何不存在的 id 都得到相同的 404。Worker 消失的 ticket 現在會在發出者持續輪詢,或(對 processing 的 ticket)佇列重新投遞該工作時以 failed 結束(背景不會有任何東西定期掃描 ticket),代碼為 ct.iac_apply_interrupted、ct.iac_apply_not_started(只有輪詢會以此結束)或 ct.iac_apply_timeout;佇列提交失敗則是 typed 的 ct.iac_dispatch_failed(HTTP 500 與 ticket 內都是),不再回傳 provider 文字。遇到任何一種,請重新 plan、審查,再 apply;若你帶了 `idempotency_key`,請改用新的 key(或不帶),因為結束的 ticket 會把舊 key 繼續綁定 24 小時(重播同一份文件會續期文件綁定,之後它可能比 24 小時更久),用它重送同一份文件只會重播那張失敗的 ticket。

  • POST /private/module/custom_tables/{scope}/tables/iac/apply
  • GET /public/task/{ticket_id}
調整IaC

IaC apply 的 idempotency_key 綁定整份文件(#2111、#2118)#

同一份文件(以完整位元組比對)搭配同一個 key 會回原本的 ticket(202),而且在 plan 之前就找到它,所以重試一個已改動系統的 apply 不再因 plan_stale 失敗。已綁定的 key 搭配會被 dispatch 的不同文件,會得到 409 ct.iac_idempotency_key_reused;新文件請使用新的 key。

  • POST /private/module/custom_tables/{scope}/tables/iac/apply
調整欄位型別

欄位更新預覽的准入改為每個資料表與每位使用者,且不洩漏無權讀取相依表的大小(#2052)#

每個解析後的資料表一個 non-blocking preview,加上每位使用者跨資料表與 scope 一個(409 preview_busy 會指出是資料表還是使用者;租約 30 秒後到期)。呼叫者無權讀取的相依 aggregate 表現在回通用的 409 aggregate_limit_violation,不再回洩漏其大小的 422 preview_projection_too_large。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/columns/{column_id}/preview
調整查詢

紀錄匯出有位元組上限與產生時間預算,並回傳具型別的錯誤(#2123)#

會超過位元組上限(預設 512 MiB)的匯出回 `413` `ct.export_too_large`;產生時間超過時間預算(預設 60 秒)的匯出回 `422` `ct.export_time_budget_exceeded`。兩者的 body 都是 `{"detail":[{"code","detail"}]}`,不會送出任何檔案位元組;部署端可以調整上限,但不會超過 1 GiB 與 90 秒。請縮小篩選或拆分匯出;100,000 列上限的 `400` 維持不變,仍是純文字字串。關閉連線會取消正在進行的產生作業。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/export
調整權限

在 list、search、export 與 saved view apply 中,隱藏欄位的回應與不存在的欄位完全相同(#2123)#

`filters`、`sort_by` 或 `sort` 命中隱藏欄位的 key,現在得到與不存在的 key 相同的 `400`(`Filter column …` / `Sort column … does not exist in table schema. Valid columns: […]`),而且 `Valid columns` 清單只列出呼叫者有權讀取的欄位。`computed_filters`、`stored_filters` 與 `any_of` 對隱藏與不存在的欄位一律回 `Column '<name>' does not exist in table schema`,隱藏欄位也不會再先被欄位型別的拒絕洩漏。saved view 的舊版 `filters` 或 `sort_by` 若指名隱藏欄位,apply 不再失敗,而是當成該欄位已被刪除。請不要依回應措辭判斷欄位是否存在。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/records
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/view
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/search
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/export
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/views/{view_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/views/{view_id}/records
調整複合指令

撰寫名為 approval_provenance 的 Command output 會被拒絕(#2055)#

`outputs` map 含有 `approval_provenance` key 的 Command definition,現在在建立、更新與 validate 時會以 `422 command_output_reserved` 拒絕,帶有它的 IaC `command` 行也會以同一個 code 讓 plan 失敗。請改用其他 output 名稱。先前已儲存的 definition 仍可執行,但再次儲存它,或在 IaC 文件中宣告它,會被拒絕,直到改用其他 output 名稱為止。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/validate
調整維運

Bulk ticket 在 worker 消失時以型別化狀態結束(#2135)#

Bulk insert、update、delete 或 upsert 的 ticket 不再永遠停在 processing。已授權的 GET /public/task 輪詢會把它終結為 failed,error_code 為 ct.bulk_interrupted(lease 已失效:第一次輪詢先記下,至少三個 lease 週期後的輪詢才終結,預設約在最後一次續租後六分鐘)、ct.bulk_not_started(一小時內從未被認領,沒有寫入)或 ct.bulk_timeout(超過兩小時),並回報已 commit 的計數。已 commit 的列保留;相同的 idempotency_key 只會重播該 ticket,不會續跑,因此請以新 key 只提交剩下的工作。輪詢到的 body 不會顯示 ticket 現在儲存的 lease 狀態、operation、table_id 與 actor_id;company_id 現在在 worker 第一次寫入進度後仍會保留。

  • GET /public/task/{ticket_id}
調整維運

GET /public/task 只對 Custom Tables ticket 要求憑證(#2085、#2118)#

後端 5.20.1 對每一次 task 輪詢都做驗證;自 5.21.0 起只有 Custom Tables ticket(以及少數僅限發出者的平台 task)需要。ct-bulk ticket 對擁有它的公司任何使用者開放;publish commit 與 IaC apply 的 ticket 只對啟動它們的使用者開放(原本就是如此),自 5.21.0 起匯入 extraction ticket 也是,而它與 IaC apply ticket 的 id 現在是 version-8 UUID。每一次拒絕都是與未知 id 相同的 404 Task not found,而 Custom Tables 內部儲存 key(匯入 session、鎖、清除與預覽 ticket、冪等結果)一律不提供,擁有者也不例外。請用上傳者的 token 輪詢 extraction ticket。

  • GET /public/task/{ticket_id}
修正IaC

IaC request 不再卡住 API worker;大型文件的 plan 更快(#2048、#2096、#2106)#

Plan、apply、state 與 export 改在 API worker 的 event loop 之外執行,apply 期間的全程序垃圾回收停頓不再凍結 worker,plan 也改為批次授權並 memoize command diff。Plan 輸出不變,呼叫端不需要任何改動。

修正IaC

IaC 對欄位 spec 中的 raw table id 與 ref 一視同仁地授權(#2121)#

以 raw target_table_id 或 source_table_id 指定表格的欄位 spec,以及 lookup 的 fallback.table,現在需要與 ref 相同的讀取權限。不存在、屬於其他公司、超出可及範圍或讀不到的 id 都回相同的 403 iac_permission_denied,回應不再透露是哪一種情況。

修正IaC

IaC Command 可以寫入名為 secret_ref 的業務欄位(#2058)#

secret_ref 只保留給 definition.callbacks 的各個 entry。step 中同名的欄位是一般欄位,不會再被「callback secret_ref must address its callback name」拒絕,也不會從 definition 中被丟掉。

修正IaC

被中斷的 IaC 欄位刪除在公司與部門文件中可以收斂;已被取代的表格上的 absent 欄位規劃為 orphaned(#2113、#2117)#

在公司或部門文件中,absent 的 column 行若其欄位已離開 schema、cell 或 state row 卻還在,現在會規劃 delete 並附 warning,apply 會完成 cell 清理(聊天室文件不會這樣收斂)。若 state row 指向的表格已不存在或已被同一 ref 取代,該行會在自己那一行規劃 orphaned,且不改動任何東西。

修正IaC

decimal unit 設定中值為 null 的 key 不再算 drift(#2111)#

IaC 會在比較前丟掉 unit 與 length_unit 中值為 null 的 key,所以 {"kind":"currency","fixed":"TWD","unit_column":null} 等同已儲存的單位,規劃 noop 而不是 update。

修正IaC

欄位新增不再在交易內執行 index DDL;型別變更不再建立 index(#2111、#2118)#

欄位新增移除了多餘的執行期 index 與 ANALYZE 維護,型別變更時建立 index 的未使用優化器也已刪除,所以失敗的欄位新增在 IaC apply 重試後可以收斂。呼叫端不需要任何改動。

修正欄位型別

背景新增欄位與同步新增一致(#2116)#

兩條路徑以相同方式 coerce 預設值,並把每筆紀錄 history 歸屬於執行操作的使用者;同步新增會在 default_value_applied 回應儲存後的預設值,背景新增則回 null。背景新增的座標欄,在新增當下表格最多 10,000 筆 live 紀錄時分批建立索引,超過則維持待處理。

修正維運

欄位新增、更新與刪除不再卡住 API worker 或遺留表鎖(#2105)#

過去較長的欄位新增會阻塞所屬 worker,直到平台將它終止,使表在鎖 TTL 內以 423 維持鎖定。現在工作在 request loop 之外執行,結束時釋放鎖。

修正權限

unique 衝突錯誤不再點出隱藏欄位(#2067)#

`unique` 或 `unique_tuple` 規則的 `409` 以前會列出每個 component 欄位與作者寫的規則名稱,即使呼叫者讀不到其中一個。現在只要有 component 被隱藏,`columns` 就只列出讀得到的 component,`constraint` 是不透明的 `rule_<hex>` id,並以 `message: "Unique constraint conflict"` 標示被遮蔽的 body,`conflicting_record_id` 與 `key_tuple` 也不再給;沒有任何 component 讀得到時,body 只剩 `{message}`(`unique_tuple` 另有 `error`)。要判斷回應是否被遮蔽,請檢查 `message: "Unique constraint conflict"`,不要檢查 `constraint`。被遮蔽的舊式 `unique` 衝突在 agent 端以 `denied` 而非 `conflict` 送達。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/records/{record_id}
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/rules
修正查詢

XLSX 紀錄匯出把每個 cell 寫成純文字,並能處理控制字元(後端 #2104、#2115、#2123)#

像 `=1+1` 或 `#N/A` 這樣的標頭或 cell 以前會變成真正的公式或錯誤 cell,使用者可控制的欄位名稱或值因此成為試算表公式注入的途徑;cell 或資料表名稱中的控制字元、U+FFFE 或 U+FFFF 也可能讓匯出以 500 失敗。現在每個 cell(含標頭)都是純文字,XML 無法承載的字元會從 cell 中丟棄,工作表則依 Excel 的命名規則以資料表名稱命名(31 個 UTF-16 code unit,`History` 改為 `Sheet1`)。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/export
修正權限

競爭同一個 unique_tuple 鍵的併發 bulk insert,落敗者得到帶型別的 409(#2066)#

以前,插入同一個 `unique_tuple` 鍵的併發 `records/bulk` 請求,可能因共用同一份 sort-slot 快照而以伺服器錯誤失敗。現在 slot 在鎖定下讀取,只有一個請求勝出,每個落敗者回帶型別的 `409 unique_tuple_conflict`。其他的 bulk `500` 請視為非預期。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk
修正IaC

IaC record 的一般 unique_tuple 衝突會帶 code unique_tuple_conflict(#2066)#

IaC apply 中重複的 record create 或 update 以前讓 `IacLineError.code` 為 null,一般的 `unique_tuple` 衝突現在會帶 `unique_tuple_conflict`,hash 碰撞或 claim 無法取得的拒絕則仍為 null;該行仍在 HTTP 200 的逐行 apply 回應內(`applied: false`,資料列、version 與 history 都不變),detail 文字不變。

修正複合指令

Staged change 與 Command 歷史清單改為先分頁取 key(#2033)#

`GET …/staged-changes` 與 Command 執行歷史清單改為先排序並計算 id,再載入所要求那一頁的 payload,所以 staged change 帶有極大 JSON 或 TEXT payload 的資料表也能正常列出,而不是失敗。篩選、排序、總數與遮蔽規則都不變,用戶端不需要改。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/staged-changes
  • GET /private/module/custom_tables/{scopeWithId}/commands/{command_id}/executions
修正複合指令

精確的整數與 decimal `$single` 值可下推進 SQL 子句(#2033)#

整數或 decimal 欄位上的 `$single` 現在可以成為後續 `select` 中精確 SQL 算術的運算元,也可以與這類算術或幣別/UOM 欄位比較;這些組合以前會被 `422 unsupported_clause_expression` 拒絕(與一般 decimal 欄位的單純比較本來就可以)。幣別或 UOM 欄位上的 `$single` 會保留單位(跨單位為 `ct.unit_mismatch`),`expr` 是計算運算式的 dimensional `$single` 仍然被拒絕。

修正複合指令

資料庫 flush 階段失敗的核准釋放現在會進入終態(#2056)#

釋放已核准的 staged change 或 Command plan 在 flush 階段失敗時(例如 unique_tuple 衝突),失敗處理程式本身會在已損壞的 session 上拋錯,change 因而一直停在 applying。現在會先 rollback session 再記錄失敗,change 以一般的拒絕原因結束為 apply_failed,並通知申請者。沒有新的狀態或代碼。

修正維運

Custom Tables 請求不再卡住 API worker,worker 回收會等待背景工作(#2140、#2139、#2122、#2136)#

Custom Tables 的資料庫工作現在改在 API worker 的 event loop 之外執行,full-GC 暫停也不再卡住請求,所以一個很重的請求不會再拖住健康檢查或其他租戶;達到 max-requests 上限的 worker 也會先完成處理中的背景工作再回收(預設最多等待 15 分鐘)。請求與回應的形狀都沒有改變。

2026-10-05

已合併,尚未部署到測試環境
4 新增1 調整2 修正

僅 Custom Tables 的來源補齊:從 7c41cbc5 至 backend 41f8a03ce359141802c8c17dccf89bfdf49bea6e(VERSION 5.18.0),增補前次 checkpoint 後的 #1965 收據/事件投影與 #1952 scope package/input 基礎。說明已合併來源,不代表部署或新增產品/瀏覽器驗收。離線 catalog/OpenAPI pin 不變,該 checkpoint 的座標 backend PR 尚未完成。請見 10 月 6 日 PR1 發行目標增補;空間查詢篩選預計在後續版本提供。#1907 在 #1952 前已 revert;full-state snapshot/rebuild/restore 仍延後。

新增IaC

Scope 套件生命週期與不可變輸入基礎(#1952)#

JWT scope-ID 路由取得 retained input,以 digest、request key 及目前權限綁定 package plan/apply/run/activate。Apply 回 202;啟用須 ready_to_activate 與 verification 通過。僅 A1–A6 基礎,A7 full-state snapshot/rebuild/restore 仍延後;不是一般 IaC 或 attachment blob 路由。

新增欄位型別

綁定既有欄位 identity 的更新預覽(#1941)#

Moderator 對既有欄位 preview 路徑 POST ColumnUpdate,不修改資料;審閱後 PATCH 保留的 body,沒有 hash/state pin。Preview 每 checked table 上限 200 live row、2,000 ms、每 tenant/table 一個 concurrent preview;無效更新使用 HTTP 拒絕。PATCH missing column 改回 404。

修正複合指令

凍結 Command 路徑的受保護原生審批事實(#1896、#1906、#1921)#

terminal_fact 映射由核心與業務交易一起寫入。mutation staging 前,純輸出受保護載體不強制 caller 可讀;真正映射輸入及現行讀取 ACL 仍有效。不可讀證據使回傳收據僅有狀態,不擴大權限。

修正複合指令

巢狀 provenance fail closed 與僅 metadata 的成功事件(#1965)#

巢狀 approval_provenance key(含 null)使回傳收據僅有狀態,manager 也一樣;頂層 null 本身不會如此。Definer authority 下,具權限的原始 invoker 只可見不含 provenance 的 succeeded-event metadata;owner、可見性、evidence 與 replay 檢查仍在,不改寫 legacy stored audit。

新增欄位型別

明確公式 v2、儲存結果及 Command 版本一致性(#1865、#1890、#1900)#

省略 formula_version 仍為 v1。V2 支援型別化同列精確 scalar、擴充函式及版本化工作日,stored 須明確選用。Commands 使用選定欄位版本;日曆衍生維護不允許編輯 commands_only 業務欄位。

新增複合指令

原子有序子集合(#1861、#1877)#

已儲存 v2 Command 使用預期版本、伺服器擁有的 parent/order cell 及分組歷史,儲存/刪除主表明細。陣列取代成員,省略 key 保留。REST、可攜 IaC 及 CT Agent/MCP 讀取遵守現行 ACL;寫入保留 Command 同意/權限界線。

調整IaC

可攜 ACL key、有界相依與其餘 CT 更新補齊#

含 #1893 可攜欄位 ACL/Command grant ref、#1892 的 256 表相依閉包(不提高直接寫入上限),及已合併 CT 維度、指派、過帳/保留、events/inbox/outbox、完成投遞、CT 觸發 Worker、公開讀取投影、publish-session owner 與附件介面。非 CT 與只有測試的變更不當作呼叫者新功能。

2026-10-03

已合併,尚未部署到測試環境
9 新增3 調整1 修正

已合併,尚未上 staging。來源 pin:teamsync-backend 7c41cbc50170b3003ba16ee099f1db4e78f61d1a(VERSION 5.13.0)。本次僅更新 Custom Tables,涵蓋 #1811、#1812、#1828、#1830、#1834、#1839、#1840、#1851、#1853 的 CT 部分、#1855、#1856、#1857、#1859。以工具機械更新的離線來源 snapshot 有 404 個路徑、500 個操作、484 個 schema。沒有查詢 staging/production,不宣稱已部署或新完成產品/瀏覽器驗收。

新增維運

判斷分隔字元並以原始自然鍵匯入(#1840)#

CSV 擷取辨識逗號、分號、tab、直線,保留原始文字/BOM/多行界線。同步 import-v2 session 支援 insert、upsert、update_only 與有界原子寫入。預設原始 key 與選用 declared_unique 仍不同;既有檔案 map/create 維持非同步、只新增。

新增複合指令

Service credential 與驗證輸出綁定單一 Command(#1853)#

JWT 管理者為有效 service account/room 與 restricted、definition-authority、effect-identity Command 簽發僅一次回傳明文的 ctsp 憑證。只接受綁定的 execute/execution-read 路徑。CT 輸出回呼使用封裝 Run capability、精確 inputs 與目前人員來源;釘住第一個有效 digest,拒絕變更輸出,仍以既有 executor/審批契約為準。

新增欄位型別

Decimal 可選用幣別/計量單位維度(#1856)#

ct.units major 1 在儲存 decimal 接受一個固定值或欄位 selector,並傳播相容的計算維度。不執行轉換;缺少/不相容/無效單位有型別化 400,受保護設定/型別轉換/selector 變更有 409。REST/IaC 保留中繼資料;既有無單位 decimal 維持 available。

新增維運

暫存僅追加多表世代並一次啟用(#1857)#

ct.generations major 1 在部門與含明確 ID 的公司範圍新增 create/status/chunk/activate/abort。2–20 張表的私有輸入維持不可見;完整 digest/總計、目前定義/權限通過後,才一次原子啟用並保留收據。不取代有效列、不繞過受治理寫入、不新增 Agent/MCP/IaC generation 工具。未知結果後請查狀態。

新增維運

載入宣告鍵參照圖與匯出租戶 bundle(#1859)#

JSON migration 使用精確宣告 unique/unique_tuple key;session 匯入可選用。原生 link 解析與一般子列共用有界原子參照圖。儲存 mapping 保留最多 64 個不可變版本,重驗目前 schema/權限。管理者專用租戶 bundle 回報截斷、懸空參照、不支援識別及不可攜 principal;重新匯入仍用各 scope 的 IaC,不是自動安裝器。

新增複合指令

正式寫入前凍結提案,保留操作收據(#1811)#

ct.proposals major 1 在三種 Command 範圍新增 prepare、提案讀取、明確 operations、收據讀取與 query selection。準備會回滾業務模擬、保留 Review 義務,核准不會正式寫入。相同 key/提案重播依目前權限回原有 operation;ct.operation_outcome_unknown 時查 operation_id。外部 callback 傳遞與資料庫 commit 分開;CT Agent 政策工具只準備,MCP 保留確認。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/prepare
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/operations
  • GET /private/module/custom_tables/{scopeWithId}/proposals/{proposal_id}
  • GET /private/module/custom_tables/{scopeWithId}/operations/{operation_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/selections
  • GET /private/module/custom_tables/{scopeWithId}/selections/{selection_id}
新增權限

跨寫入途徑約束精確群組總和與筆數(#1828)#

aggregate_limit 是第 18 種規則:0–4 個 group key、integer/decimal sum 或 count、<=/>=/==,以及 literal、數值群組或 linked bound。Principal-blind 的目前鎖定狀態包含舊群組與目標 bound。Deferred 在一次原子 Command 最終狀態檢查,包含審批重播,或在暫存世代啟用時檢查;其他寫入仍立即檢查。預覽/admission 包含現有列,aggregate_limit_violation 不揭露數值。IaC 與 CT 工具 schema 指引保留相同契約。

新增查詢

依時間讀儲存版本,依截止值排名 lookup(#1834)#

As-of 端點在帶時區 RFC3339 截止時間(含該時間)選最新儲存版本,依目前列/表/欄位與目標 link 權限讀取。計算值省略、delete 版本是 tombstone,舊 link 擷取缺漏回 409 history_links_unavailable。MCP records_history 與 Analyst 提供讀取;lookup pick.as_of 則先篩選目前 linked rows 再排名,IaC 轉譯來源欄位 ref。不代表新增 revision 欄位型別或獨立 capability ID。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/records/{record_id}/as-of
調整權限

彙總守衛錨定精確、有序的 partition(#1851)#

aggregate_limit.guard_partitions 正規化後須完全等於同順序 group_by,省略就預設使用它。持久化 generation/群組錨點涵蓋空/新群組、舊/新成員與 linked-bound 供應端;IaC 轉譯相同 ref。這是避免 phantom 的約束執行,不是任意鎖分片。

調整維運

呈現 purge 範圍,不刪除保留證據(#1855)#

Table/tag 預覽新增 16 個非負資源計數與明確保留 blob/Command 證據標記。保護 blocker 使同意 ticket 為 null。15 分鐘 ticket 是同步、一次性,先消耗再檢查執行者/標的/變動;runtime 計數不是結構綁定。Tag 的 purge_blocked 可包裝已消耗 ticket 的拒絕,請重新預覽,不要假設可重用。

調整查詢

使用驗證過的 typed 值,不擴大查詢權限(#1830)#

儲存 integer/float/decimal/string/date/datetime predicate 可用交易內驗證 projection,仍保留租戶/列/欄位 ACL 與完整 predicate。既有 integer/float 精度、精確 decimal 字串、數值 aggregate 與文字分組語意維持相容。Projection 缺少/過期/衝突或 probe 不可用時退回 JSON,不是租戶拒絕。型別/運算子與精確 decimal 拒絕仍保留,不宣稱 benchmark 或延遲保證。

修正權限

單筆讀取的不存在與隱藏使用同一回應(#1812)#

單筆 record、hydrated 與 incoming-links 讀取,不存在/軟刪除及目前隱藏皆回 404 {detail: "Record not found"}。修正列是否存在的措辭洩漏,不是統一所有 not-found 途徑;資料表/範圍權限錯誤仍不同,trash-restore/bulk 措辭也仍不同。

2026-10-02

已合併,尚未部署到測試環境
3 新增2 調整7 修正

已合併,尚未上 staging。來源 pin:teamsync-backend 3e2b00eb5e7ea89ed36eb92559e42b739ef86c55(VERSION 5.13.0)。本次僅更新 Custom Tables,涵蓋 #1793、#1794、#1795、#1796、#1803、#1806、#1791/#1792 的 CT 部分,以及指定的既有措辭更正。離線來源 snapshot 有 353 個路徑、440 個操作、413 個 schema。沒有查詢 staging 或 production,不宣稱已部署或新完成產品/瀏覽器驗收。

新增維運

選擇啟用公司業務時區(#1795)#

ct.calendars major 1 新增 GET/PUT company/{company_id}/core/calendar。公司管理員設定標準 IANA business_timezone,可用 expected_version 做版本保護。日期評估固定使用同一時間點與日曆版本;排程明確設定的時區優先,null 則保留原有時鐘,不重寫既有日期。

  • GET /private/module/custom_tables/company/{company_id}/core/calendar
  • PUT /private/module/custom_tables/company/{company_id}/core/calendar
新增欄位型別

String 欄位的持久編號序列(#1795)#

頂層 sequence(IaC 為 spec.sequence)宣告持久的公司 series、顯示 pattern、日期/reset bucket、最多兩個維度及 gapless/allow_gaps 政策。產生的儲存格不能自行提供或修改;既有列不補編號、已發出編號不回收,restore/revert 保留目前編號。待核准 Command 不保留編號,只顯示 (assigned at release);不安全的計算用途為 sequence_value_computed。

新增權限

開放期間規則與期間變更稽核(#1795)#

period_fence 是第十七種規則,date_column、period、open 必填。Link 或 range mode 依操作檢查前/後影像與目前期間;409 ct.scope_closed 不暴露期間列 ID。變更已關閉期間需授權的人員 Command 理由或核准釋放,period-events 依可讀範圍回傳。這是可覆寫的規則,不是永久不可變。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/period-events
調整維運

有序 atomic bulk 與 prepare 預先檢查(#1803)#

同步 actions 批次依提交順序在同一交易執行,沒有 ordered flag;後續 action 看得到先前暫時效果,並依當下版本檢查 expected_version。任何拒絕都全部回滾,包括 action 進入保留時的 approval_routing_changed。Agent/MCP prepare 在確認 token 前檢查 prospective batch,不保留狀態;commit 重新檢查。非同步部分成功工作仍是另一條路徑。

調整權限

衝突資訊依可讀範圍揭露(#1796)#

unique 只對可讀的衝突揭露 conflicting_record_id/key_tuple,省略不可讀欄位名稱;unique_tuple 維持不含值。一般不存在/隱藏資料列檢查用 {detail: "Record not found"},own 寫入他人資料列只有可讀時回 403。REST bulk 較早的 own/filtered pre-pass 與 trash 未刪除列的 restore 字串仍為明列的來源例外,不宣稱所有 404 都一致。

修正維運

排程中的 bulk 寫入重讀目前規則(#1793)#

Insert/update worker 在資料表鎖內重讀 period、count、interval 規則情境,包括提交後才 commit 的規則。非同步 API shape 不變;部分失敗仍是 ticket 逐列錯誤,不代表整個工作 atomic。

修正欄位型別

同步 retype 一起 commit 資料列、歷史與 schema(#1794)#

同步欄位 retype 在資料表鎖內以一個資料列/歷史/schema 交易執行,並行加入的規則不能留下被禁止的 schema。REST 對超過 10,000 列的資料表走背景路徑;此門檻不代表非同步原子性,IaC 欄位更新仍同步。

修正AI 工具

CT bulk ticket 需具公司歸屬(#1803)#

REST ct-bulk- status 需要可用的獨立公司 binding 與經認證的相符公司;binding 不存在/過期/屬其他公司時回 404 Task not found。MCP 優先用 body.company_id,progress body 被替換後才以獨立 binding 作 fallback;兩者皆缺或 owner 屬其他公司時為 not_found。MCP 計數依目前 worker 欄位映射,不再把 failed 一律報為零。

修正IaC

四種格式錯誤輸入改為 typed 且不含值的拒絕(#1806)#

IaC grant 的 list/object access level 使用 grant_access_level_type;v2 step kind 的 list/object 使用 command_step_kind_type;UTC expiry 溢位使用 public_read_valid_until_overflow。REST Command/public-read issue 只含 type/loc/msg。IaC 以 400 line error 中止 parse;超過安全上限(最多 4300 位)的整數也以 json_integer_too_long 拒絕。其他驗證維持 422,不承諾無關的伺服器錯誤都消失。

修正維運

CT 匯入保留 XLSX 儲存值與 literal 儲存格(#1791)#

CT 匯入使用 XLSX 儲存值與 literal 儲存格,不執行公式。精確小數 preserve_text 與舊 .xls 的 exact-source 拒絕不變;請提供具有預期儲存值的 XLSX,或使用 CSV。

修正AI 工具

CT 工具匹配與 LINE 通知對齊(#1792)#

LINE group/room 情境的 CT 工具可匹配 line、line_group、line_room 設定;CT 權限通知使用設定的 LINE API base URL。

修正維運

更正既有 trigger/IaC 教學與繁體中文措辭#

更正 token 精確匹配、trigger ID 生命週期/type_changed、可選 chatroom_id、webhook 的 source_permission_context/schedule、receipt、巢狀布林與未知 sandbox row ref。更正 IaC 分階段訊息、client_access 共用 ref 及工作台 line/ref/phase/detail 教學;明列指定的本機驗證邊界近似。這些是文件更正,不是新宣稱的產品修正。

2026-10-01

已合併,尚未部署到測試環境
1 破壞性4 新增2 調整3 修正

已合併,尚未上 staging。目前來源 pin 為 teamsync-backend origin/master dcb288d069454854f7ff68dc485e593734ccdf84;VERSION 為 5.13.0,不代表部署。本區塊涵蓋 2f548ae2 之後的 #1738、#1774、#1776、#1781、#1788,以及 #1774 取代 #1766「IaC 移除 trigger 多餘 key」說法的更正。重新產生的 CT snapshot 有 349 個路徑、435 個操作、400 個 schema。沒有查詢 staging 或 production 端點,不宣稱瀏覽器驗收或部署已完成。#1784 僅變更測試工具。

破壞性IaC

IaC 在 parse 階段拒絕未知的 trigger 與 action key(#1774)#

trigger 行的 spec 只能帶 id、name、on、when、transition、schedule 與 actions;每個 action 只能帶 type、id 與其類型自己的 key(webhook:url、headers;api_call:method、url、headers、body;其餘列在 trigger 行頁面)。其他任何 key 現在都會在 parse 階段讓整份文件以 HTTP 400 `line N: [...]` 失敗:回傳 `extra_forbidden` issue,其 loc 是 ["trigger", "spec", key] 或 ["trigger", "spec", "actions", i, key],最多三則、依 key 排序,且不會執行任何 plan。#1766「IaC 移除 webhook/api_call action 合約以外的 key」的說法已被取代:這些 key 不再被靜默移除,而是被拒絕。未知的 action type 沒有任何允許的 key,所以 type 與 id 以外的每個 key 都會被拒絕。

你要改什麼移除 400 所指出的 key(loc 會給出 key,或 action 索引與 key),再重新 plan。
  • POST /private/module/custom_tables/{scope}/tables/iac/plan
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
新增權限

資料表可設定只接受透過 Command 寫入紀錄(#1788)#

新增能力 ct.action_policy(major 1,由 GET .../core/capabilities 列出,並帶有 `exemptions` 陣列:table_lifecycle、platform_operator_maintenance、schema_migration)。資料表的 settings.write_policy 是 `any`(預設)或 `commands_only`;moderator 可在建立資料表時、以 PATCH .../tables/{table_id}/write-policy({write_policy} → {table_id, write_policy})或在 IaC table spec 中設定。commands_only 下,Command 以外的每一筆紀錄寫入都會在任何東西被暫存或記錄之前,以 403 `governed_context_required`({error, message, table_id, operation?, surface?})拒絕:REST 單筆與批次寫入、匯入、restore 與 revert、reorder、publish session、callback、測試資料、trigger、IaC record 行(plan 的 validate 錯誤,apply 時再檢查)、agent、MCP(在 dry run 就拒絕)、agent workspace 寫入與 sandbox write-back(422 `output_policy_governed_table`)。Command 仍可寫入。資料表 trash、restore 與 purge、平台操作人員維運與 schema migration 屬於例外,並以 ct.write_policy.exemption log 稽核。這個 policy 管的是路徑,不是讓資料列變成不可變:moderator 可以把它改回 `any`。

  • PATCH /private/module/custom_tables/{scope}/tables/{table_id}/write-policy
  • POST /private/module/custom_tables/{scope}/tables
  • GET /private/module/custom_tables/{scopeWithId}/core/capabilities
新增複合指令

Command 可限制為只有 execute grant 才能執行,並以自己的 ceiling 執行(#1788)#

Command 帶有 `execute_policy`:`open`(預設,與現行行為相同)或 `restricted`——只有每張被引用資料表的 manager,以及持有 execute grant 的主體能執行。Grant 以 GET/POST .../commands/{command_id}/grants 與 DELETE .../grants/{grant_id} 管理(principal_type 為 user、department、chatroom 或 client)。沒有 grant 的呼叫者會得到 403 `command_execute_denied`,在列表中看不到該 Command(GET 為 404);不是所有資料表 manager 的 grantee 看到的是 execute-only 檢視(definition 為 null、definition_redacted 為 true、interface)。restricted 的寫入型 Command 還可宣告 `authority: definition` 與 `table_access` ceiling(最多 20 張資料表,逐操作設定,永遠不是 manager 權限):它改以該 ceiling 而非呼叫者的權限執行,作者會被記為 authority owner 並在每次儲存時重新推導,非 manager 的呼叫者拿到的是 status-only receipt(redacted: true)。新的錯誤:403 `command_execute_denied`、`definition_authority_invalid`、`definition_ceiling_missing`;422 `definition_requires_restricted`、`definition_ceiling_invalid`。IaC command spec 帶有 execute_policy、execute_grants(最多 100 筆)、authority 與 table_access;在 IaC 中省略 execute_policy 即 open,省略 execute_grants 會撤銷所有 grant。

  • GET /private/module/custom_tables/{scopeWithId}/commands/{command_id}/grants
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/grants
  • DELETE /private/module/custom_tables/{scopeWithId}/commands/{command_id}/grants/{grant_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
新增複合指令

有審批閘門的寫入可排除送審者自己核准(#1788)#

require_approval rule 或 Command definition 上的 `exclude_requester: true` 會禁止送出該 plan 的人自己核准。若排除送審者之後,某個必要的閘門沒有任何可核准者,送出會以 409 `no_eligible_approver`(附 gate_order)拒絕;若送出的主體沒有經過驗證的真人身分(例如 service account),則回 409 `requester_unattributable`;release 時的重新檢查若發現核准者就是送審者,會丟棄該 plan(原因 requester_cannot_approve)。這個旗標只在為 true 時儲存,rule 中的非布林值是 rule 設定錯誤(`require_approval exclude_requester must be a boolean`)。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/rules
  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
新增權限

date_interval_exclusion rule 讓日期期間不重疊(#1738)#

`date_interval_exclusion` 類型的 rule 指定必填的已儲存 `start_column`、另一個可為 null 的已儲存 `end_column`,以及一到兩個基數為 one 的 `scope_columns` link,且不能有 `when`。期間為左閉右開 [start, end):相鄰的期間沒問題,end 為 null 表示無終點,同一組有序 scope 的兩個 live 資料列不可重疊。所有寫入路徑都受它約束(REST 單筆與批次、匯入、publish session、IaC record 行——以 200 內的 per-line IacLineError code 回報——、Command、callback、trigger、agent 與 MCP),拒絕時回不含值的 409(publish session 則在暫存時以 400 `invalid_rows` 拒絕該列):`date_interval_exclusion_invalid_period`(「Date period is invalid.」)、`date_interval_exclusion_overlap`(「Date period overlaps another period in this partition.」)或 `date_interval_exclusion_retry`(「Date period rules changed during this write; retry.」)。建立時必須明確寫出 end,可以是日期或 null。PUT rules 會先檢查 live 資料列,並以相同代碼回應;這條 rule 計入每表 10 條的上限;把起始欄位改成選填、把結束欄位改成必填,或 purge scope link 所指向的資料表,都會以 `date_interval_exclusion_dependency` 被拒絕(purge 則是同代碼的 blocker)。有這條 rule 的資料表,寫入會在其 write guard 上序列化。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/rules
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/rules
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
調整AI 工具

Agent 遵守 write policy、execute grant 與日期期間(#1738、#1788)#

對 commands_only 資料表的 agent 原始紀錄寫入,現在會在任何 proposal、確認或 receipt 之前,回傳結構化的 `governed_context_required` payload(error、message、table_id)。execute_policy 為 restricted 的 Command 只對可以執行它的呼叫者出現在 agent 的 roster 與工具中。資料表頁面在 rule 的欄位都可見時,會列出 `Visible date interval exclusion policies JSON`。Agent 會轉述的錯誤代碼清單從 48 增為 55(最新的是 command_execute_denied、definition_authority_invalid、definition_ceiling_missing、governed_context_required、no_eligible_approver、requester_unattributable 與 command_failed)。MCP 紀錄工具在 dry run 就以 `governed_context_required: <message>` 拒絕受管制的資料表。

修正IaC

IaC validator 失敗改為帶型別的 400,不再是 500(#1774)#

丟出一般錯誤的 validator,例如帶有 spec 的 `state: absent` 行、或帶有憑證的 callback URL,以前會讓請求以 500 中止。現在會以 `value_error` issue(msg 為 `Value error, <message>`)出現在 plan 與 apply 相同的 `line N: [...]` 400 中,非同步 apply ticket 則出現在 parse_error。Parser 回傳的每則 issue 除了送出的 input 之外也不再帶 `ctx`,所以 400 內文只剩 type、loc、msg 與 url;最多回傳三則 issue。

  • POST /private/module/custom_tables/{scope}/tables/iac/plan
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
修正複合指令

Command 的寫入與 returning expression 在儲存時驗證(#1776)#

SQL aggregate 運算元與行內 computed 運算元 $rollup、$lookup、$formula,出現在 `update` 的 set 或寫入 step 的 returning 的任何位置,現在會在 Command 建立、更新或 validate 時被拒絕:422 `unsupported_clause_expression`(phase author),並附上該運算元的 path。以前它們可以通過儲存,直到實際寫入一列時才失敗。儲存時也改為對真正編譯後的 expression 檢查寫入值:寫進 decimal 欄的純十進位字串 literal 被接受(超出的小數位數在寫入該列時仍會被拒絕,先前「cannot be written to the target column type」的拒絕對這類 literal 不復存在),格式錯誤的 principal literal 則以 `Principal cell literal must be user:<id>, smc:<id> or room:<id>` 拒絕。寫入值檢查的 path 現在是 steps[<i>].values|set.<欄位 key>,不再是固定的 "path": "expression"。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/validate

2026-09-30

已合併,尚未部署到測試環境
1 破壞性2 新增2 調整6 修正

已合併,尚未上 staging。目前來源 pin 為 teamsync-backend origin/master 2f548ae2a71a613df1afed29044e3fce83209542;VERSION 為 5.12.0,不代表部署。本區塊涵蓋 d48281f2 之後的 #1753、#1755、#1757、#1758、#1761–#1763、#1766、#1768,以及 decimal $to_string 文件修正。重新產生的 CT snapshot 有 340 個路徑、423 個操作、391 個 schema。沒有查詢 staging 或 production 端點,不宣稱瀏覽器驗收或部署已完成。#1765 僅變更測試工具。

破壞性IaC

舊式 XLS 來源不可匯入 decimal 欄(#1763)#

Company、department、chatroom 匯入若將 .xls(BIFF)映射至既有 decimal 欄或新建 decimal 欄,會在建表或寫列之前拒絕。HTTP 400 的字串 detail 為「Decimal XLS import is unsupported because extraction may round cells; use UTF-8 CSV or text cells in XLSX with preserve_text_cells=true」。佇列中的 XLS/XLSX 匯入 worker 會重新檢查即時目標 schema;若目標改為 decimal 而來源不精確,會以 error_code custom_table_import_exact_source_required 結束為 failed,不寫入任何列。

你要改什麼重新擷取 UTF-8 CSV,或搭配 preserve_text_cells=true 的 XLSX 文字儲存格;重試佇列匯入前重新檢查 decimal 目標。
  • POST /private/module/custom_tables/{scope}/tables/from-import
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/import
新增維運

操作人員可清理舊憑證儲存(#1766)#

POST /root/hotfix/custom-table-trigger-secrets-scrub 預設啟動跨租戶 dry run(execute=false、batch_size=200,允許 1–1000)。execute=true 以每頁一個交易、具冪等性的方式改寫舊 IaC trigger 基準、遮罩機制導入前的 command 基準與 schema_triggers_update history 列,保留 updated_at。以 GET .../{ticket_id} 輪詢 processing/completed/failed 與各儲存區 scanned/needs_rewrite/rewritten 計數;狀態列 TTL 為 24 小時,失敗只暴露清理過的例外型別/code。即時 trigger/command 設定與讀取回應不變;來源可用不代表任何環境已執行遷移。

  • POST /root/hotfix/custom-table-trigger-secrets-scrub
  • GET /root/hotfix/custom-table-trigger-secrets-scrub/{ticket_id}
新增複合指令

Command effect identity:業務事件只記錄一次(#1768)#

新增能力 ct.effect_identity(major 1,由 GET .../core/capabilities 列出)。版本 2 的寫入型 command 可宣告 effect_identity {namespace, kind, key};key 由有型別的 literal 與 command 在第一個寫入之前所選資料列的不可變 id 或 table_id 組成。在 REST、agent、MCP commands_execute 與 trigger invoke_command 上,同一個事件、相同的驗證後 inputs 與程式,會 replay 原本的 receipt(replayed: true、原本的 execution_id、呼叫者無法完整讀取或 replay 無法重新檢查而被扣留、並列在 redacted_outputs 的 outputs)且不寫入任何東西。inputs 不同或程式改過是終局的 409 ct.effect_identity_conflict;列範圍權限無法重新證明存取是 403 ct.effect_identity_replay_forbidden;$first key 為空或被略過是 422 effect_identity_unbound;無效的區塊是 422 effect_identity_invalid。第一個事件記錄之後,變更或移除 identity 在 REST PUT 是 409 effect_identity_locked;IaC apply 則 rollback 該行,並在 apply 結果中以該行的 IacLineError.code effect_identity_locked 回報。有審批閘門的 command 在 release 時 claim,所以重複的 release 以 apply_failed 結束。Claim 是資料庫紀錄,不依賴 Redis,idempotency_key 的行為不變。被 claim 參照的執行不會被營運人員的 prune 清除刪掉,POST .../execute 在 OpenAPI 中也重新宣告 403。沒有這個區塊的 command 不受影響。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • GET /private/module/custom_tables/{scopeWithId}/core/capabilities
  • POST /root/hotfix/custom-table-command-executions/prune
調整觸發器

IaC state 與新 history 寫入遮罩 trigger 憑證(#1755、#1766)#

last_applied_spec 宣告中的 webhook/api_call 憑證 slot 以 __SECRET__ 儲存;下次 scope/system apply 結束也會清理該範圍的舊 state 列,不更動 updated_at 或使 plan hash 失效(#1755)。新的 REST trigger PUT 與 IaC history 寫入,現在會先遮罩憑證再儲存 schema_triggers_update 列(#1766)。#1766「IaC 移除 webhook/api_call action 合約以外的 key」的說法已被取代:自 #1774 起,這類 key 在 parse 階段即以 400 拒絕(見 10 月 1 日條目 iac-unknown-trigger-keys-rejected)。Apply 未觸及的舊列需操作人員清理,延遲 state 清理仍保留。讀取回應原已遮罩,格式保持不變。

調整觸發器

公開 callback 的 idempotency 改為 durable receipt(#1768)#

POST /public/module/custom_tables/callback/{token_id} 的 idempotency_key 防線,現在是與資料列在同一個交易寫入的資料庫 receipt,而不是 Redis 標記。key 在 receipt 視窗內有效——預設 24 小時,部署可用 CUSTOM_TABLE_CALLBACK_RECEIPT_TTL_SECONDS(1–86400 秒的整數)縮短——會 replay 原本的結果並設 replayed: true,含被送去審批的票據。相同 key 搭配不同的 op、record_id 或 data,現在是 409 {"error":"idempotency_key_reuse"} 且不寫入(以前會 replay 第一次的結果、忽略改過的 body);視窗過後該 key 當成新的一次執行。Redis 中斷不再跳過去重,寫入已 rollback 的被拒絕嘗試不會留下 receipt。沒有 key 的已簽請求在簽章有效(±300 秒)期間只會被送達一次;沒有 key 的 Bearer 請求仍不去重。較舊的 pod 只記錄在 Redis 的 receipt,會在它們的 24 小時結束前繼續回應。

  • POST /public/module/custom_tables/callback/{token_id}
修正AI 工具

還原與 Agent 寫入依目前 schema 驗證(#1753)#

REST history restore/revert、MCP records_restore 與 Review replay 會在產生效果前重新驗證目前欄位/規則。衝突回具型別的 409 RecordRestoreConflict409Response:approval-gate detail,或含 removed_column、missing_required_field、type_mismatch、invalid_value 的 conflicts。Agent insert/update/bulk 在 confirmation token 之前預檢,與 REST 的 integer half-even 正規化、空白 required/decimal 拒絕與 interval 驗證一致;interval 提案會顯示界限,欄位說明經清理與長度限制。Analyst interval 的 overlaps/contains_point/presence predicate、grouped/any_of 子句與起點排序/分頁可用;Agent filter model 拒絕未知欄位與 presence 的非 null operand,REST 不變。

修正AI 工具

MCP 執行公司、附件與 saved-view 合約(#1757)#

tables_create/tables_bootstrap 在 scope_kind=company 時,會於角色檢查之前以 not_found 拒絕其他公司,沒有副作用。attachments_upload 要求紀錄 edit 權限,只接受可見 attachment 欄(隱藏:not_found;錯誤型別:invalid_argument),會附加檔案,單檔欄則取代。views_save update 省略 is_shared 時保留分享設定,create 未帶 name 會在 dry run 拒絕。

修正權限

資料表讀回省略 passphrase_hash(#1758)#

REST table detail/list/discovery/shared/readable-tables、MCP tables_get/tables_list/tables_shared_with_me 與 Agent tool 的 table 回應,都省略 settings.client_access.passphrase_hash。passphrase_enabled 與其他 settings 不變;客戶端不可預期 hash 欄存在。

修正IaC

匯入命名選擇公司可用模型(#1761)#

CUSTOM_TABLE_IMPORT_LLM_NAMING=true 時,每個匯入工作從匯入公司的可用 entry 選一次模型。沒有可用模型或任何選擇/命名失敗時,沿用依標題產生的確定性名稱;source_columns 與 column_map 來源 key 仍使用原標題。

修正欄位型別

Chatroom 向上計算讀取與 decimal preview 使用有效公司(#1762)#

Chatroom table 向 department/company 目標的 rollup、lookup 與符合條件的 lookup fallback,會解析有效公司並回傳可讀值,不再因 table.company_id 為 null 不相符而回 null。Link preview 依同一公司驗證目標 scope。計算 preview 的 decimal 溢位或無效運算,回 400,detail 字串帶計算錯誤。

修正複合指令

修正 decimal $to_string 文件#

精確 decimal 儲存格以保留宣告 scale 的標準十進位 wire 字串進入實際 Command 路徑,因此 $to_string 保留精確文字。先前 9 月 29 日文字誤把內部 Decimal 物件 fallback 當成使用者可見限制;此處與原條目已修正。Float 儲存格仍依值格式化為 2.5、2.0 或 1e+16。

2026-09-29

已合併,尚未部署到測試環境
4 破壞性6 新增2 調整10 修正

已合併,尚未上 staging。來源 pin 為 teamsync-backend origin/master d48281f258a7f12efd3e3fbbc2ab93d545b65df0;該提交的 VERSION 檔內容是 5.12.0,不代表部署或發行版本。本區塊涵蓋 CT PR #1723、#1732、#1740–#1746、#1748、#1749、#1751、#1752,以及 70862ceae..d48281f2 區間其他影響 CT 的提交。OpenAPI snapshot 由此 master checkout 產生(362 個路徑、458 個操作、451 個 schema);沒有查詢 staging 或 production 端點。此處的核心能力已在合併後的原始碼中可用;不宣稱瀏覽器驗收或 staging 部署已完成。尚未合併的 PR #1738 不列入本區塊。

破壞性觸發器

Trigger 設定限管理者讀取,對外傳送的密碼欄位改為唯寫#

三種 scope 的 GET triggers 現在都要求資料表管理者權限。GET/PUT、MCP、history 與 IaC 的讀回內容會將 webhook/api_call URL 的 userinfo、query 值、header 值及 API call body 的常值葉節點遮為 `__SECRET__`;具型別的 `secrets` slot 只指出位置與有無值。非管理者不會收到 trigger 設定的 history 列。PUT 或 IaC apply 時,佔位符或省略的秘密 slot 只能在同一 trigger/action、同一有效目的地保留原值;新值會取代,明確的 null 會清除。無法安全保留時回具型別的 `trigger_secret_keep_refused`,不會寫入。URL path 仍可見,因此不可把憑證放在 path。

你要改什麼將 trigger 設定讀取權限限制在資料表管理者。不要再讀回已儲存的憑證;未更動的 slot 保留 `__SECRET__` 或省略,變更 action 目的地時重新輸入憑證。URL path 中的憑證應移至會遮罩的 slot。
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/triggers
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/history
破壞性權限

完全無權限的資料表中繼資料讀取與不存在資料表同樣回應#

依 ID 讀取資料表、欄位、自身權限、管理者、rules、triggers、staged changes 與 migration status 時,若呼叫者沒有 read、edit、insert 或管理者權限,現在會得到與資料表不存在相同的 404。只有 insert 或 edit 權限的呼叫者仍可讀取中繼資料。Migration status 對非管理者隱藏原始錯誤文字;若資料列 ACL 限制該呼叫者,也隱藏全表進度數量。shared-with-me 與名稱解析不會列出已刪除或不可讀的資料表。

你要改什麼這些依 ID 讀取的 404 應視為「不存在或無法存取」。導覽請依可讀資料表探索端點與有效權限決定;不要再從舊有 403 或未受限的 migration 數量推測資料表是否存在。
破壞性公開讀取

新的 client-access 通關密語不得超過 72 個 UTF-8 位元組#

REST 與 IaC 現在會在 bcrypt 前拒絕空通關密語或超過 72 個 UTF-8 位元組的值;字元數少於 72 但含多位元組字元也可能超限。省略或送 null 會保留現有 hash。`<REDACTED>` 只有在 IaC 匯出文件中才是保留既有密語的 sentinel;在 REST 中它是要設定的新字面密語。不會進行 Unicode 正規化或去除空白。

你要改什麼REST 或 IaC 寫入前,先以 UTF-8 位元組檢查新密語長度為 1–72。要保留現有密語,請省略或送 null;IaC sentinel 只用於 IaC。
  • PATCH /private/module/custom_tables/{scope}/tables/{table_id}/client-access
破壞性權限

部門欄位新增與診斷報告須具資料表管理者權限#

部門資料表的 POST columns 現在與其他 schema 寫入端點共用資料表管理者門檻;只有 read、edit 或 share grant 不足以新增欄位。Diagnosis 與 diagnosis-results 讀取可能含有原始資料列,現在也限該表管理者、所屬部門經理或公司經理使用;非管理者取得 403。

你要改什麼新增欄位與診斷控制項只提供給資料表管理者或相應經理。資料列 edit 或資料表 read grant 不能當成 schema 管理權限。
  • POST /private/module/custom_tables/department/{department_id}/tables/{table_id}/columns
新增欄位型別

精確 decimal 欄位與計算式 decimal 運算已在合併原始碼中可用#

`decimal` 是第 20 種欄位型別。儲存與傳輸都使用一般十進位字串,不經二進位浮點數;欄位需宣告 precision 1–38、scale 0–min(18, precision) 與 allow_negative。一般寫入若小數位超限會拒絕,不會四捨五入;接受後會正規化成固定 scale。Formula 與 rollup 運算維持精確值;decimal avg 必須明確提供 result_scale 與 rounding_mode。Agent 讀寫及 IaC 使用同一 decimal 儲存格契約。Command expression 將 decimal 視為數值型別;精確 decimal 儲存格以保留宣告 scale 的標準十進位 wire 字串進入 `$to_string`,所以它會保留精確文字。Float 儲存格維持其格式,包括 `2.5`、`2.0`、`1e+16`。能力探索將此 build 的 `ct.decimal` major 1 回報為 available,沒有 feature flag 或租戶覆寫。

新增維運

Custom Tables 新增 build 層級的核心能力探索#

chatroom、department 與 company 的 Command scope 都掛載 GET core/capabilities。回應包含 generic-core 契約、支援的 Command/IaC 文件版本、已實作能力的可用狀態、Command 限制,以及執行中 CT OpenAPI 形狀的 digest。它不授予資料表權限,也不宣稱已部署的發行版;此來源 pin 的 server_release 為 `unidentified`。

  • GET /private/module/custom_tables/chatroom/{chatroom_id}/core/capabilities
  • GET /private/module/custom_tables/department/{department_id}/core/capabilities
  • GET /private/module/custom_tables/company/{company_id}/core/capabilities
新增權限

可選用 unique_tuple rule 保護多欄位組合的唯一性#

`unique_tuple` rule 可指定 1–16 個有順序的欄位組件,每個組件都要明定 null_policy:forbidden、equal 或 distinct。cardinality-one link 可參與;文字組件可選擇不區分大小寫及 NFC 正規化。資料列與 schema 路徑都會檢查此組合,包括 Agent 與 IaC 寫入。重複值回 409 `unique_tuple_conflict`;hash 碰撞另有 `unique_tuple_hash_collision` 代碼。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/rules
新增權限

原生參照可要求來源與目標具同一個有效維度#

可選用的 `same_dimension_reference` rule 指定來源的 cardinality-one 參照 link、來源必填的維度 link,以及被選目標上的必填維度 link。兩個維度 link 必須指向同一張維度表,現有資料列也必須共用一個仍有效的 anchor;違反時回 409 `same_dimension_reference_violation`。rule 存在期間不能移除依賴的 link/資料表。GET rules 現在回傳 rules_digest;PUT 可送 expected_rules_digest,在規則已變動時以 409 拒絕過期的整份取代,省略則仍是無條件取代。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/rules
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/rules
新增權限

部門成員可列出自己能讀取的資料表#

GET department/{id}/readable-tables 允許該部門所屬公司的使用者進入,接著只列出有效權限 can_read 不為 none 的未刪除資料表。`total`、分頁與 tag 篩選都依這個可讀集合計算;沒有可讀資料表時回 200、total 0。原本只給經理的 GET department/{id}/tables 門檻仍在;能讀取不代表有管理權。

  • GET /private/module/custom_tables/department/{department_id}/readable-tables
新增AI 工具

teamsync-tables MCP server 提供受治理的資料表工具#

`/mcp/tables` 掛載 47 個 v1 工具,涵蓋依 scope 探索、資料列、history、作者建立的 Command、附件與分析。adapter 共用資料表權限檢查與 REST generic;不存在、跨租戶及禁止存取的 ID 都收斂為 `not_found`。寫入、破壞性及付費工具使用 dry_run 與一次性的 confirm_token。tables_analyze 在 specialist 背景工作中執行,可用 tables_analyze_status 輪詢。此 MCP 介面不在過濾後的 CT REST OpenAPI snapshot 內。

調整AI 工具

作者建立的 Command 向 Agent 提供有界的選擇卡片#

工具選擇器依精簡卡片挑選作者建立的 Command,卡片包含 Command 名稱、作者描述與輸入欄位描述;模型可見的參數 schema 與 REST Command 定義沒有變更。

調整AI 工具

CT 模型工作未指定模型時回傳明確的設定錯誤#

Command composer 的 REST 與 WebSocket 通道在未指定模型時會回傳 `job_model_not_configured`、工作名稱及 root pin_path;REST 回 409,WebSocket 送出 error frame 後保持連線。CT Analyst 則回 denied 結果,附相同原因與維運者應使用的 pin path。這是設定拒絕,不是資料表權限判斷。

修正維運

資料表 history 分頁加入穩定的最後排序鍵#

資料表 history 仍先依 created_at 與 version 由新到舊排序,兩者相同時再依唯一的 history id 遞減排序。一次 schema 操作產生的多列可能同時具有相同時間與版本,因此最後排序鍵可避免 offset 分頁在兩次讀取間漂移。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/history
修正AI 工具

Analyst 的 count 與 distinct 遵循 REST 的非空值規則#

Analyst 的 count(column) 現在排除缺值、JSON null 與空字串,count(*) 仍計算資料列。count_distinct 對已儲存值使用與 REST 相同的型別判斷,對 JSON path 則計算非空的純量葉節點;multi_select 比較完整清單,包含空清單。計數仍回整數。

修正權限

佇列中的 bulk insert 會在 worker 重新檢查即時寫入權限#

以使用者名義進入佇列的 bulk insert,worker 會在暫存或處理資料列之前重載身分,並重新執行資料表成員、有效權限與 insert 檢查。已撤銷或不再有效的 grant 會讓 ticket 以 `custom_table_bulk_insert_authorization_denied` 失敗,插入零列;請求當下的 ACL 值不會成為永久授權。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk-insert
修正維運

CSV 匯入保留來源文字並支援明確指定 Boolean 對應#

直接擷取 UTF-8 CSV 時只會消耗檔案開頭的 BOM,保留引號內 LF/CRLF 與儲存格文字、空白列,並補齊長短不一的資料列而不移動來源索引。重複的來源欄名仍可使用;依名稱對應時取最後一次出現,建立新表時則依位置對應。文字不會自動推測成 Boolean,但明確指定為 Boolean 的目標欄位,會在提交時轉換前後可含空白、大小寫不拘的 true/false 文字。worker 中無效的資料列保留索引錯誤,其他有效列仍可能插入。

修正維運

Purge 移除 link 時會為每筆 schema history 指派不同版本#

Purge 目標表時,若來源表有多個相依欄位要移除或降級,現在會在來源表鎖下取得下一個 schema version,並逐筆增加 history 版本。未 flush 的 MAX(version) 不再讓多筆 history 撞版號。

修正公開讀取

Callback update 文件與實際接受的欄位鍵一致#

公開 callback update 原本就接受已知的顯示名稱與內部 `col_<hex>` ID。OpenAPI 說明現在正確記載此事,也指出未知的 update 欄位鍵會回 400。此 PR 修正公開契約的文字,沒有新增 update 行為。

  • POST /public/module/custom_tables/callback/{token_id}
修正複合指令

部門 Command 審核可為有權限的跨部門呼叫者正常放行#

審核放行時,部門 scope Command 的呼叫部門現在與暫存時相同,依凍結的 Command scope 重新計算,而非拿身分的所屬部門比較。有權限的跨部門呼叫者不再僅因這個差異得到 `source_authority_changed`;即時 grant 及資料列/欄位檢查仍會執行。真正的來源授權不一致會在具型別的 `details.fields` 列出變動欄位。

修正IaC

IaC 在建立資料表前驗證管理者目標#

IaC plan/apply 會在寫入資料表前,於該表所屬公司解析每位提議的管理者;不存在、已刪除、其他公司或 service account 使用者會以 `invalid_moderator_target` 拒絕。REST 管理者寫入也拒絕 service account 目標。無效的管理者清單不會再留下已建立的新表。

修正觸發器

root CT 排程 tick 的同步工作改由執行緒池處理#

root due-trigger route 現在是同步 FastAPI handler,讓 SQLAlchemy 的 claim 與復原掃描在執行緒池中處理,不會阻塞伺服器 event loop。請求與回應契約沒有改變。

2026-09-19

5.10.11
3 破壞性6 新增1 調整2 修正

v5.10.11 已發佈到 production。來源 pin 為 70862ceae7869470c14f4ed7cc7dfc3609e299f4(版本提交,對應 tag v5.10.11);production 部署 run 35428530132 於 2026-09-19 成功,並以 GET https://api.cluster.scfg.io/openapi.json 讀回 info.version 5.10.11。本 repo 的 snapshot 由該 production 文件重新產生(358 paths、454 operations、439 schemas),且與部署中的 staging 逐位元組相符——staging 跑的是 5.10.10,它與 5.10.11 的唯一差異是本模組之外的 edge-worker 程式碼,所以下列契約在 api.scfg.io 上都已可用。Command 新增可見性、不寫入的 validate route、內建輸入範例、Custom API tool callback、兩個字串運算子與自然語言 composer;資料列新增 1 MiB 資料上限與行內二進位守門;權限清單也開始回吐已儲存的 row policy。日期採 Asia/Taipei。

破壞性複合指令

Command 新增 public/private 可見性#

CommandCreateRequest、CommandUpdateRequest、CommandResponse 與 JSONL IaC command spec 都新增 visibility: "public" | "private"。private 指令只會為建立者綁定成聊天室 agent 工具——管理全部被引用資料表的人在這條 lane 上不例外——REST 的 get、list、execute、query 也限建立者或管理者;看不到時回的是同一個不可列舉的 404 Command not found。visibility 在 PUT 是必填且沒有預設值,因為 PUT 是整筆取代且不會改寫 created_by,若有預設值,資料表管理者就能悄悄把別人的指令改成非 private。IaC 匯出在預設值時省略這個 key,而 IaC lane 本身刻意不依可見性過濾——看不到 private 指令的 plan 會把它判讀成不存在而規劃刪除。

你要改什麼每一次 command `PUT` 都要帶 `visibility`。若要維持 v5.10.11 之前的行為,請送回 `GET` 取得的值(舊指令可明確送 `"public"`);省略現在會得到 422。IaC 中省略 key 代表 `public`,所以必須保持 private 的指令,每次 apply 都要帶 `"visibility":"private"`。
  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • GET /private/module/custom_tables/{scopeWithId}/commands
  • GET /private/module/custom_tables/{scopeWithId}/commands/{command_id}
破壞性查詢

資料列的 data 上限 1 MiB——413 record_too_large#

每次寫入都會以實際要儲存的 dict(compact JSON、UTF-8)對照 CUSTOM_TABLE_MAX_RECORD_BYTES——預設 1 048 576,可依部署覆寫——超過就回 413 {"error":"record_too_large","message","limit","size"}。適用於單筆建立/更新、bulk insert/update 與 mixed create/update action、command insert/update 步驟、歷史還原、測試資料插入與 publish 前置檢查;schema migration 對既有列的改寫則豁免。更新時量的是合併後的資料列,且只拒絕「超過上限之後還變大」,body 會附上 previous_size,所以既有的超大資料列仍可編輯。需審核的寫入在送進暫存之前就檢查,因此過大的受審寫入在送出當下被拒,而不是核准後套用才失敗。非同步 bulk lane 會把它記成該列的 ticket 錯誤,agent 工具則把 413 對應成終止性的 record_too_large 結果,讓模型改為縮小 payload 而不是重試。

你要改什麼每筆實際儲存的 record `data` 都要維持在部署回傳的 `limit` 以內;同步寫入要處理 `413 record_too_large`,非同步寫入則要處理逐列 ticket error。檔案 bytes 請移到 attachment/blob 並只存 id;既有列若已超標,在縮小或拆分完成前不要再讓它變大。
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/records/{record_id}
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/{record_id}/history/{version}/restore
破壞性查詢

欄位中的行內二進位回 400 inline_binary_not_allowed#

在量測大小之前,欄位帶有 base64 data URL 或裸 base64 blob 的寫入會先被拒絕:400 {"error":"inline_binary_not_allowed","column","reason","message"}。順序是刻意的:過大的 base64 data URL 會拿到指出解法的 400,而不是單純的 413。reason 為 data-url(值以 data: 開頭且前 256 字元含 ;base64,,不論長度)、data-url-embedded(文字中的 ;base64, 後接 ≥ 1024 個 base64 字元)、base64-run(≥ 4096 個 base64 字元且大小寫混合含數字,多行時還需 MIME/PEM 折行形狀)或 base64-magic(通過相同的大小寫混合加數字守門後,≥ 1024 字元且以已知檔案 magic 前綴開頭;這一支不要求 MIME/PEM 折行形狀)。一般文字、URL、JSON 文字,以及貼上的 id、數字、十六進位或 DNA 清單都不會命中。更新時只檢查有變動的欄位,且既有的葉字串會被略過,所以原樣送回既有的二進位欄位不會擋住編輯。

你要改什麼不要再把 base64 data URL 或裸 base64 檔案本體寫進 record cell。請透過 blob API 上傳檔案,再把回傳的 blob id 存入 attachment 欄位;遷移期間,未變動的既有二進位葉節點仍可編輯,但任何新加入的二進位葉節點都會被拒絕。
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/records/{record_id}
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk
新增複合指令

POST /{scope}/commands/validate——不寫入的建立檢查報告#

新的 route 會完整跑一次 POST /commands 的閘門——定義編譯、管理權限、callbacks、tag 歸屬、名稱唯一性、內建範例——並回傳 CommandValidationReport,但不寫入任何東西。errors[] 每一筆都帶著 POST 會拋出的 status 與 detail,順序也與 create 相同,編輯器可沿用既有分支;與 POST 不同,它最多收集 50 筆而不是在第一個失敗就停止。advisories[] 是不屬於錯誤的事實與注意事項:目前需審核的寫入目標、要到執行時才檢查 scope 歸屬的 identity input,以及一定會出現的 name_uniqueness_unlocked(因為驗證不會取命名空間鎖)。compiled 回報被引用的資料表、mode、寫入目標、其中需審核的子集、宣告的 input 名稱與 agent tool 名稱——後者由佔位 id 組成,所以結尾一律是 _00000000。valid: true 不是保留。

  • POST /private/module/custom_tables/{scopeWithId}/commands/validate
新增複合指令

自然語言指令 composer,含 REST 與 WebSocket 兩條通道#

/commands/composer 下的五條聊天室 scope route,讓使用者用自然語言描述指令,由伺服器端 agent 起草、驗證、預覽並儲存。POST /chat 是非同步的——一次回合要 30–120 秒,比 edge 願意保持回應的時間還長——所以它回 202 {thread_id, turn_id, poll_path, poll_after_ms},再由 GET /turn long-poll(wait_seconds ≤ 60)取得帶有相同 ComposerChatResponse 欄位的結果;WebSocket 的 turn_result frame 另外帶有 frame discriminator:"event":"turn_result"。同一個 client_id 一次只能跑一個回合;並行的 POST /chat 回 409 turn_in_progress。GET /history 回傳逐字紀錄、草稿、最後一份驗證報告與待審儲存;DELETE /thread 會刪除 checkpoints 與索引項目,並 best-effort 清除殘留的 Redis turn lock/record。Thread 以 client_id 為 key,而 server 從不用它決定租戶歸屬。核准後的儲存會以呼叫者身分走一般建立閘門,所以可能和 POST /commands 一樣失敗。因為 socket route 不會出現在 OpenAPI,GET /ws/docs 公開 WebSocket 契約;socket 以短期 signature 認證,不是 Bearer token。另有兩條 CloudManager 專用 root route 管理持久狀態:建立 checkpoint 資料表,以及預設只預覽的閒置 thread 清理。沒有 checkpoint 儲存時,POST /chat、GET /history 與 DELETE /thread 回 503 composer_unavailable,socket 以 1013 關閉;只讀 Redis 的 GET /turn 與只提供文件的 GET /ws/docs 仍可使用。

  • POST /private/module/custom_tables/chatroom/{chatroom_id}/commands/composer/chat
  • GET /private/module/custom_tables/chatroom/{chatroom_id}/commands/composer/turn
  • GET /private/module/custom_tables/chatroom/{chatroom_id}/commands/composer/history
  • DELETE /private/module/custom_tables/chatroom/{chatroom_id}/commands/composer/thread
  • GET /private/module/custom_tables/chatroom/{chatroom_id}/commands/composer/ws/docs
  • POST /root/hotfix/custom-table-composer/checkpointer/setup
  • POST /root/hotfix/custom-table-composer/threads/prune
新增複合指令

生命週期 callback 可以用 id 觸發聊天室 Custom API tool#

CommandCallback.url 改為可省略,並新增 CommandCallback.api_tool_id;兩者必須剛好設定一個。使用 api_tool_id 時,method 與 headers 取自 tool 本身,body 則為必填。明確指定非 POST 的 callback method 或任何非空 headers 都會被拒;為了讓正規化後的內容可往返,明確/預設的 POST 可以存在,但執行時仍會以 live tool method 取代。接著會把 tool 的 method、url、headers 物化進私有執行快照並渲染 {{var}} 系統變數(datetime、chatroom_id、client_id、user_id、department_id、company_id),因此 tool 的 URL/header 憑證不會進入定義;作者提供的 body 仍會儲存在定義中,IaC 也把它當作 secret material。Tool 必須在自己的聊天室、已啟用、method 為 POST/PUT/PATCH、沒有 query_params、沒有 {{@…}} agent 參數、沒有未知 {{var}}、host 不是樣板、url 與 headers 沒有字面 $,且 url 通過 SSRF 檢查;所有不成立的原因都會在建立時回報。執行當下 tool 不存在、已停用或屬於別的聊天室時,該 callback 連同指名它的 trigger 一起被丟棄,並列在 skipped_callbacks。IaC 匯出保留 tool 參照並遮蔽 body;callback 密鑰 digest 也涵蓋 api_tool_id。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
新增複合指令

definition.examples——最多三筆 agent 會照抄的輸入範例#

定義可帶 examples: [{name, inputs}](最多 3 筆)。每一筆在建立時都以編譯後的 inputs 驗證,用的是與 /execute 相同的那套——失敗時回 400 {"error":"command_example_invalid","example","index","message"}——因此範例不可能與定義脫節。第一筆會以有界、單行且經過 sanitization 的 tool 參數 JSON 寫進聊天室 agent tool description。Input 遭拒時,只有 compact serialization 不超過 1,500 字元才附上精確 inputs 物件,否則封包帶 example_omitted: "too_large"。command_input_invalid 另外會帶 field、expected_inputs(名稱、型別、是否必填、item_schema)與 hint。宣告了 item_schema 的 array input 會以具型別的項目清單暴露給模型,而不是 list[Any],執行時的驗證仍由引擎負責。這個 key 與 mode 一樣「未設定即省略」,所以沒有範例的定義維持位元相同;JSONL IaC 的 v1→v2 匯出升級也會把 examples 帶過去。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
新增複合指令

新增運算子 $to_string 與 $pad_start#

$to_string 把整數、浮點數、布林、date、datetime 或字串算成字串——清單、link、attachment、json 與 principal 在建立時就被拒絕,null 仍是 null。$pad_start 接受 [value, width, fill],用單一字元 fill 把 value 左補到 width 個字元;width 是 0–64 的整數。字面 width 超出範圍、或 fill 不是剛好一個字元,都是建立時錯誤;非字面的則在執行時做範圍檢查,超出時回 null,因此結果型別可為 null。算出來的字串是決定性的:整數是十進位數字、浮點數是最短可還原形式、布林是 true/false、date 與 datetime 是 ISO 8601。兩者都沒有 SQL 下推,所以運算元非常數時,子句矩陣會在建立時就拒絕把它們放進 where 之類的 SQL 子句,而不是之後才失敗。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
新增權限

權限清單回傳 read_filter、edit_filter 與 visible_columns#

GET .../permissions 的 ExplicitGrantEntry 與 GET .../department-permissions(三個 scope 掛載點皆是)的 DepartmentPermissionResponse,現在會直接從儲存的資料列帶回 read_filter、edit_filter 與 visible_columns。對應等級不是 filtered 時 filter 為 null;沒有存過投影時 visible_columns 為 null,而這裡的 null 代表全部欄位,不是沒有欄位。visible_columns 裝的是內部欄位 id。這只影響回應:grant 的撰寫、解析與強制執行方式完全沒有改變。它補上的缺口是——前端讀不回自己寫過的東西,於是把每一筆 grant 都鏡射進一個資料列欄位。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/permissions
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/department-permissions
調整維運

BlobInfoWithSignedUrl 的說明改為描述附件下載的實際行為#

Schema 說明與 signed_url/expires_at/expiration_seconds 的欄位說明改寫:signed_url 是舊欄位名稱,不代表有密碼學簽章;自訂表格的附件下載回傳的是未簽章的物件 URL,expiration_seconds 為 0;此時 expires_at 只是相容用的時間戳,不是會被強制的下載期限;取用或分享回傳的 URL 不會再跑一次端點的授權檢查。行為完全沒有改變——這個端點一直都是這樣——但從 OpenAPI 重新產生的 client 會看到 diff,而舊說明暗示了一個從未被強制的到期時間。

  • GET /private/module/custom_tables/{scopeName}/tables/{table_id}/records/{record_id}/attachments/{blob_id}/download
修正複合指令

Command 執行會保留真正的輸入驗證原因#

步驟中的資料列寫入若沒有通過 record core 自己的 input 驗證——未知或無法存取的 attachment id、該 scope 沒有的 principal id——過去會拋出理由允許清單不認得的原生 ValueError,於是 HTTP 錯誤與執行紀錄都只說 "Command execution failed"。現在這些拒絕會以 400 呈現,detail 是 core 的原始訊息(純字串),執行紀錄的 error 也存同一段文字。請直接顯示 detail,不必再為舊文字做特例。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
修正複合指令

整數型別的 SQL 投影保持整數#

整數欄位是以 NUMERIC(65, 30) 下推,所以對它們取 MAX 與 SUM 會回傳被算成浮點數的 decimal。宣告為 integer 的 let 因此會拿到 2.0,配上新的 $to_string,第二個流水號就被算成 CASE-2026-02.0。現在投影型別為 integer 的值會被正規化回整數;float 型別與沒有型別的值維持原本的呈現。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/query

2026-09-10

5.10.0
2 新增1 調整2 修正

v5.10.0 已通過 staging E2E 並發佈到 production。來源 pin 為 9b8b95e598a7b1f606c030d8dd0f27f26c748c83(版本提交;執行程式碼與已驗收的 9393591ae 相同)。Private 資料列新增可編輯性資訊,查詢可篩選編輯/待審狀態,部門匯入前置作業改依公司歸屬授權;API snapshot 與雙語範例同步至部署中的 OpenAPI。 日期採 Asia/Taipei;staging 標記表示這些 API 已可在測試環境使用,production 上線則由 v5.10.0 release 明確確認。兩列 Excel 流程、拒絕寫入與撤權以 staging 驗證,production 的映像/版本/健康另行讀回確認。

新增權限

Private 資料列提供目前的 can_edit#

Private 資料列回應必定含 can_edit,表示目前資料列的 edit ACL。它與審核暫存獨立,也不略過值、欄位、規則與授權檢查;更新及 command post-image 會依新持久化的資料列重新判斷。 前端請讀此旗標,不要從角色、建立者或 insert 權限推測列可編輯性;審核狀態與之後的寫入拒絕仍應分別處理。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/records/{record_id}
新增查詢

以 editable 與 pending_approval 篩選可讀列#

Private POST search body 接受兩個獨立、可省略的布林值;GET list 不接受這兩個 query parameters,於 total、排序與分頁前以 AND 套用。editable=false 包含 own-only edit grant 下沒有建立者的列;尚無實體列的待審新增不在其中。已儲存 view config、匯出與 public-read 不接受這兩個本次請求欄位。 前端的篩選分頁請使用 POST search;把這兩個名稱加進 GET query string 不會篩選結果。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/search
修正權限

部門匯入的上傳與預覽不再要求管理權限#

已登入使用者可在同公司部門進行匯入前置作業;map 仍要求目標表 insert ACL,建立新表仍要求部門管理權限。Session 的 scope 綁定及撤權後的拒絕仍有效;issue #1261 已通過真實 XLSX staging E2E。 前端可對同公司使用者提供上傳/預覽,再分別判斷 create 與 map 權限;上傳成功不是寫入授權。

  • POST /private/module/custom_tables/department/{department_id}/import/extract
  • GET /private/module/custom_tables/department/{department_id}/import/{sid}
調整AI 工具

小型查找回傳可見欄位,確認使用業務名稱#

find_records 的 match 現在包含 id、label 與 data,簡單文字查找不必再呼叫 analyst。Analyst 的 lookup mode 使用一次篩選查詢,衍生指標仍需驗證;欄位提示說明型別與限制,批次工具只宣告允許的動作,遭拒的異動不會被說成已寫入。

修正複合指令

Command 結果型別與布林 null 語意對齊 compiler#

Execution column 的 value_type 包含 user、social_client、principal,對齊 compiler 已允許的 scalar join。執行期 $and/$or 採 SQL 三值邏輯:false AND null 為 false、true OR null 為 true,沒有決定性布林值則維持未知;請重新產生結果型別並保留 metadata,不要把未知值轉成 false。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/query

2026-09-05

5.8.4
2 新增5 調整6 修正

IaC 身分 token 現在可用於 `principal` 欄位的 cell(teamsync-backend PR #1161,以 origin/master HEAD 3012a2940bce3085f5c354e925ed16c595e1a185 合併,VERSION 5.8.4,staging 已上線)。`principal` cell 可以寫成 `$user:<username>`、`$smc:<platform>:<platform_user_id>` 或 `$room:<聊天室名稱>`,並存成該欄位要求的帶標籤 cell;export 則依標籤把這些 cell 反向對應回 token。`$dept:` 仍然只用於 grant。PR #1155 與 #1166 只是 staging E2E 測試套件,沒有路由、schema 或 ORM 變更。同一天的第二波把手冊同步到 origin/master HEAD b4baa266c478f0f5c2a8cea09f8f2078d112bab5,也就是 staging 上正在服務的那個 commit(teamsync-backend PR #1171、#1172、#1173、#1178、#1179、#1181、#1182、#1184、#1187、#1188、#1190、#1191、#1192、#1193)。pin 是那個 commit,不是某個發行版:該 HEAD 上 VERSION 檔的內容是 5.8.4,那是檔案內容,不是發行版宣告。agent 通道多了可寫入的 `$me` token、每個 envelope 只留一個 record id,計算欄位的過濾與排序改為跟著 principal 而不是分析 scope,異動 receipt 會保留模型自己的回答,agent 寫入通道上的 4xx 變成終局拒絕而不是內部錯誤,明確沒有寫入的 command 拒絕會說明原因,資料表清單的 `record_count` 會被呼叫者的 row ACL 收窄,整段對話都會載入的協定新增了運作模型段落,而同時啟用兩種資料表 scope 的房間也不再重複載入該協定兩次。這一波沒有新增路由也沒有新增 schema——兩個 pin 之間,過濾後的 Custom Tables OpenAPI 文件位元完全相同。

新增IaC

`principal` cell 開始接受 `$user:`、`$smc:` 與 `$room:` 身分 token#

`principal` 欄位上的 `record` cell、trigger `when` 值與 link 自然鍵現在都可以帶可攜身分 token,於 plan 與 apply 兩側以公司為錨點、fail-closed 解析,並存成帶標籤 cell——`$user:alice` 變成 `user:<id>`、`$smc:line:U123` 變成 `smc:<id>`、`$room:Support Desk/Tier 1` 變成 `room:<id>`。`user` 與 `social_client` 欄位不變,仍然存裸 id。`$dept:` 只用於 grant,放進任何 cell 都是 kind 閘門錯誤 `principal token '$dept:Sales' does not match the column's type: a principal column expects $user:<...> or $smc:<...> or $room:<...> or a raw id`;原始帶標籤 cell 仍然原樣通過。若 record 行用到的 principal 欄位是同一份文件較早處宣告的,分類會延到 apply:屆時壞掉的 token 回報 `200` 與 `applied: false`、`principal column '<internal key>': …`,而不是 422。

  • POST /private/module/custom_tables/{scope}/tables/iac/plan
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
新增AI 工具

`$me` 在 agent 通道上成為可寫入的值,並在驗證前解析#

`custom_tables_insert_record`、`custom_tables_update_record`,以及 `custom_tables_bulk_record_actions` 的 insert 與 update 兩個分支,現在都接受把 `$me` 這個 token——去除前後空白、不分大小寫、僅此一種寫法——寫進人員 cell(teamsync-backend PR #1190)。工具箱會在驗證**之前**把它改寫成當事人,因此驗證器、CRUD 歸屬閘門與資料庫看到的都是普通 cell;儲存用的 cell 文法沒有改變,也永遠不會存進這個 token。在 `principal` 欄位上,它在內部通道變成帶標籤的 `user:<id>`、在外部 client 通道變成 `smc:<id>`;在舊的 `user` 或 `social_client` 欄位上則變成裸 id,而且只在該欄位自己的通道上成立——無法解析時 token 會原樣留著,被一般形狀檢查擋下,而不是寫成別人。伺服器沒有同義詞表:「我」「me」「myself」是模型的責任。租戶歸屬與存活檢查仍然套用在解析後的 id 上。REST 不變,仍然要求已解析的 id 或 `ref`。

調整IaC

Export 會把 `principal` cell 反向對應成 token,對應不出來則保留原始帶標籤形式#

Export 不再原樣輸出每一個 `principal` cell。它會先解析該 cell,再用它自己的標籤查對應的表——`user:` 查使用者、`smc:` 查 social client、`room:` 查聊天室——聊天室裸名在公司存活房間中唯一時輸出 `$room:<聊天室名稱>`,否則在該組合唯一時輸出 `$room:<部門名稱>/<聊天室名稱>`。對應不出來的一律維持原始帶標籤 cell:兩種形式都模稜兩可的聊天室、名稱本身含 `/` 的聊天室、被軟刪除或懸空的聊天室、`(platform, platform_user_id)` 對到多列的 `smc`,以及跨租戶 id。同一個未變動系統的兩次匯出位元相同;帶有原始帶標籤退路的匯出,在原本的 scope 重新 plan 依然是零差異。

  • GET /private/module/custom_tables/{scope}/tables/iac/export
  • POST /private/module/custom_tables/{scope}/tables/iac/plan
調整AI 工具

模型看得到的 envelope 只留一個 record id,「找不到」也會導向下一步#

每一張表在 `record.data` 裡都帶著一個自動產生的 `id` 主鍵欄位,它的 uuid 與工具實際定址用的 `record_id` 是不同的值。現在在 agent 通道上,insert 與 update 結果、`custom_tables_query_records` 的分頁,以及其中深度 1 的 link 展開,都會拿掉那個 `data.id` cell(teamsync-backend PR #1190);REST 回應仍然保留它,因為前端就是用它定址 cell。統一的「找不到」拒絕現在也會導向下一步——`Record '<id>' not found or has been deleted. Re-read the record id from the insert or find result and retry the update with it; never re-insert the row.`——而且它是固定文字,401、403、404 完全相同,所以這條通道依然不是存在性的 oracle。兩項變更源自同一起事故:agent 從 insert 結果複製了 `data.id`、把 update 花在它上面、讀到「找不到」,然後把整列重新 insert 了一次。

調整AI 工具

`computed_filters` 與計算欄位的 `sort_column` 改為跟著 principal,而不是分析 scope#

計算欄位的**值**本來就會查 principal 的完整即時權限表;但 `computed_filters` 與計算欄位的 `sort_column` 這兩條通道仍然在模型自選的 `table_ids` 下 deny-default,於是在窄的分析 scope 裡,你讀得到某個 rollup 卻不能對它過濾或排序。現在這兩條通道改用同一個不受 scope 限制的解析器(teamsync-backend PR #1173);而且 filter、sort 與渲染儲存格共用同一個 term builder,三者不可能各自漂移。deny-default 沒有改變,而且是無聲的:principal 真的讀不到的來源仍然解成 `can_read: "none"`,計算項會編譯成 SQL `NULL`——每個值運算子都比對到零列、`is_null` 會命中、排序鍵落在 MySQL 預設的 NULL 位置。沒有錯誤也沒有拒絕訊息,所以計算欄位過濾「沒有結果」並不代表沒有這樣的資料列。

調整AI 工具

異動 receipt 改為附加在模型回答之後,不再整段取代它#

伺服器過去會把模型寫的成功或失敗敘述整段換成有界 receipt。現在它保留模型的回答,並把 receipt 附在下方——模型的話在前,伺服器的事實在後(teamsync-backend PR #1182 與 #1188)。當敘述留了下來、**而且**每一個 mutation step 都套用成功時,footer 是一句括號附註:「(已為你新增 1 筆資料)」/`(1 record added for you)`;同一輪既新增又更新時,只渲染一句合併附註「(已為你新增 1 筆、更新 1 筆資料)」/`(1 record added, 1 updated for you)`。其他情況維持獨立句——「資料已新增完成。」/`The data was added successfully.`——兩個以上相同的已套用 step 且沒有留下敘述時,則計數成「已新增 3 筆資料。」/`3 records were added.`。只有與實際結果矛盾的句子會被拿掉;敘述被整段丟棄只有一種情況:這一輪並非全部套用、且同時綁定了 staged-execute 工具。被同一輪後續一次成功寫入取代的拒絕或失敗嘗試——`record_id` 相同,或寫錯 id 重試時 payload 相同——完全不會渲染;站得住腳的拒絕則渲染成「這項變更沒有執行,資料沒有被改動。」/`This change was not made, and nothing in your data was altered.`

調整AI 工具

Customer-interaction tip 改為不指定語域,協定則寫明自己的運作模型#

每一則 Custom Table ToolMessage 都會帶的那句提醒,現在是 `Speak in the room's own voice and language; answer the person's question naturally. Never expose JSON, record/table ids, UUIDs, tokens, digests, URLs, or system headers — describe things by their business names and keep every business value exact.`(teamsync-backend PR #1187)。舊的寫法讀起來像在指定語域,只因為讀了一張表,就把 LINE 上的育兒助理推進客服口吻;這則提醒只談資料揭露。整段對話都會載入的協定也補上了對應段落:Voice 段(persona、語言與語氣決定**怎麼說**,協定決定**可以說什麼**)、「How A Table Works」(scope 與可見性、結構、各欄位型別的值形狀、原子寫入——身分只存在於 `record_id`)、「How A Command Works」(定義、一次全有全無的執行、模式與政策、結果、權限——teamsync-backend PR #1192),以及被削減成只講後果的 Permissions 條目:結果送到手上時已經收窄,不推論也不解釋權限,拒絕就是政策(teamsync-backend PR #1191)。`custom_tables` 與 `custom_tables_department` 共用同一份協定檔,`custom_table_commands` 有自己的一份。

修正查詢

空 cell 不再從 REST search 與 agent 工具組的 `neq` 漏出(也不再被 `eq "null"` 選中);`is_null` 在每一條通道上意思相同#

MySQL 會把省略的欄位存成明確的 JSON `null`,unquote 之後是字串 `null`。REST `stored_filters` 的純量編譯與 agent 工具組的兩個 filter 主體直接拿這個字串比較,因此 `status neq "closed"` 會回傳每一列沒有 status 的資料、`status eq "null"` 會選中它們——string、text、select、principal、user、social_client、boolean 欄位都受影響。Command 的 query 通道與 row policy 本來就會把空 cell 擋掉,所以同一個 filter 在不同通道答案不同(teamsync-backend PR #1169)。現在三條通道的每一個值運算子都經過 presence gate,而且 `is_null` / `is_not_null`(PR #1170)在所有通道上都把 key 不存在、明確的 JSON `null`、空字串 `""` 視為相同——`json` 欄位例外,JSON 的 `""` 在那裡是真實的文件,只有 key 不存在或 JSON `null` 才算空。數字、日期與 `json` 欄位本來就有 gate,維持不變。沒有任何 wire 形狀改變,只有原本錯誤的列集合被修正。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/search
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/query
修正AI 工具

Agent 寫入通道上的 4xx 是終局拒絕,不再是「An internal error occurred」#

從 CRUD 呼叫逃出來的 4xx——最常見的是 row policy 的寫入後檢查 `403 This record is outside your editable scope.`——過去到 agent 手上會變成泛用的內部錯誤訊息;那讀起來像暫時性問題,會誘發一次不可能成功的重試。現在寫入工具會依序對應(teamsync-backend PR #1184):`scp_*` 的 detail 保留自己的 SCP 代碼;已淨化的 lock 409 保留 `{"error":"write_conflict","code":"write_conflict",…}` 且沒有 `status` key;401/403/404 收斂成 `{"status":"denied","code":"mutation_denied","error":<統一文字>}`;其他任何 409 變成 `{"status":"conflict","code":"mutation_conflict",…}`;400/422 變成淨化後的驗證文字並帶 `status: "failed"`;其餘(含 5xx)維持泛用訊息。拒絕 envelope 的 `error` 依呼叫形狀統一——有指名 record 的呼叫拿到會導向下一步的「找不到」,沒有指名的拿到 `This change is not allowed with the current table permissions.`——所以 401、403、404 仍然無法分辨。

修正複合指令

明確什麼都沒寫入的 command 會告訴客戶原因#

有五個 command 結果代碼屬於明確沒有寫入,因為每一個都伴隨 rollback 拋出:`assertion_failed`、`cap_exceeded`、`cardinality_failed`、`command_input_invalid`、`command_rule_failed`。它們面向客戶的 receipt 現在會講出原因,而不是停在「失敗了」——「這項變更沒有套用,資料沒有被改動:未通過商業規則檢查」/`This change was not applied, and nothing in your data was altered: a business rule check did not pass`(teamsync-backend PR #1179)。`assert` step 失敗時,原因就是 command 作者自己寫的 `message`,在 tool result 上與 `"error": "assertion_failed"` 並列成 `reason`,會被壓成單行純文字並限制在 200 字元;另外四個代碼沒有作者文字,改用該代碼的固定後備字串。provider 或資料庫的文字永遠不會送到客戶眼前,而且「結果不確定」的分支仍然會先被檢查,所以可重試或 5xx 的失敗仍然使用「無法證明發生了什麼」的文案。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
修正複合指令

Command 寫入後列的身分 cell 會像 REST 讀取那樣被加工#

版本 1 command 逐 step 寫入後列 `rows` 中的 `user`、`social_client` 或 `principal` cell,過去是原樣回傳儲存字串;於是下一次呼叫用 REST 單筆 GET 讀同一筆資料,看起來卻像另一個物件。現在 executor 會把每一份寫入後列交給該讀取路徑所用的同一個共用加工器,並以該表自己的公司為錨點(teamsync-backend PR #1171):`principal` cell 變成 `{ref, kind, id, name, …}`,而舊的 `user` cell 維持 `{id, name, username, is_deleted}`、舊的 `social_client` cell 維持 `{id, platform, name}`——刻意沒有 `ref`、沒有 `kind`,因為前端就是靠 cell 形狀分辨這兩種欄位型別。在本租戶解析不出來的 id 會保留原始字串。相對於 column ACL 的順序與 REST 相反——寫入後列在加工執行時早已被移除過——但結果相同,因為加工器只改寫列中已存在的 key。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
修正權限

資料表清單的 `record_count` 會被呼叫者的 row ACL 收窄#

資料表清單上的 `record_count` 過去是該表未經過濾的存活列數,所以授權為 `own` 或 `filtered` 的成員等於被告知 policy 後面藏了幾列他打不開的資料。現在清單改為套用與資料表詳情計數相同的條件,並逐表解析(teamsync-backend PR #1172):表管理者、或 `can_read: "all"` 且沒有 SCP 收窄時維持完整計數,而且仍由單一批次查詢一次算完;`own` 計算呼叫者自己建立的列;`filtered` 計算編譯後的 row policy,無法執行的 policy 會讓整棵樹 fail closed;受 SCP 規則管轄但沒有任何被授權房間的呼叫者則拿到 `0`,與被遮蔽的詳情讀取一致。chatroom、department、company 三種 scope 的清單都走同一個 helper。因此清單計數是逐呼叫者的數字,不可以跨使用者快取。SCP 會自己逐表重新判定管理者身分,而不是從權限階梯上讀取。

  • GET /private/module/custom_tables/{scope}/tables
修正AI 工具

同時啟用兩種資料表 scope 的房間,只會載入一次共用協定#

`custom_tables` 與 `custom_tables_department` 兩個 job 共用同一份協定檔——它們的差別只在「哪些表在視野內」,協定本身從不指名 scope。同時啟用兩者的房間,每一輪都會收到整份檔案兩次;而且因為出現了兩段 prompt 文字,builder 還會把兩者一起降級塞進「Assigned Capabilities」外框:43,428 個字元,而單一 scope 只有 21,616,每個段落都重複一次。現在 `SimplePromptBuilder.add_job_prompts` 會跳過已經加入過的相同 prompt 文字(teamsync-backend PR #1193),因此同時啟用兩種 scope 的房間,拿到的就是單一 scope 那份、沒有被降級的單一段落;commands 協定因為是另一份檔案,仍然照常出現在旁邊一次。沒有路由、schema 或 ORM 變更,協定文字本身也沒有改動。

2026-09-04

5.8.4
7 新增4 調整3 修正

部門級動態授權(teamsync-backend PR #1127、#1135、#1141、#1147、#1153 與治理相關的 #1090–#1098,以 origin/master HEAD 6d8748b031816e19d6177644ec2245ba5542884b 合併,VERSION 5.8.4,staging 已上線)。現在一筆 grant 就能表達「業務看指派給自己的、主管看整個部門的」:新的帶標籤 principal 欄位型別、在其上解析成 principal SET 的 $me 與 $me.department、and/or/not row-policy 樹,以及沿 link 的動態授權。Agent 工具組新增 command 治理表、有上限的送達對帳視窗與兩條 root 檢視路由、聊天室解析與 principal 分組標籤。紀錄匯出不再回傳空內容。

新增欄位型別

新增 `principal` 欄位型別:一個帶標籤的 cell 指向使用者、社群客戶或聊天室#

`principal` cell 是恰好一個帶標籤的字串——`user:<id>`、`smc:<id>` 或 `room:<id>`——所以單一負責人欄位可以放內部使用者、社群客戶,或整個聊天室(該室成員都算被指派)。寫入閘門檢查租戶與存活(指派聊天室不需要成員身分);讀取回傳 `{ref, kind, id, name, …}`,而 `ref` 就是用戶端要寫回去的字串。`user` 與 `social_client` 逐位元不變,加工結果仍然沒有 `ref`/`kind`。`ColumnType` 現在共 19 個值。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/columns
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/search
新增權限

`principal` 欄位上的 `$me` 解析成 principal SET#

在帶標籤的 `principal` 欄位型別上,`$me` 不再是單一 scalar id,而是解析成一個 tagged cell 的 SET——呼叫者的 `user:<id>`,加上其所屬每個存活、同公司房間的 `room:<id>`——並透過既有的 `in` operator 編譯,因此一筆 department grant 就能同時涵蓋業務走 TS、LINE 或共用房間的情況。Social client 只解析成 `smc:<id>`、永遠不帶 room 腿,public-read token 則永遠不命中。SET 永不截斷:空集合、無法解析、或超過 1000 筆都會讓該條 leaf 帶著 `and`/`or`/`not` polarity fail closed。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/permissions
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{department_id}
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/permissions/me
新增權限

`principal` 欄位上的 `$me.department` 解析成 department SET#

`$me.department` 現在可用於 `principal` 欄位,解析成呼叫者部門的每一位存活、同公司 user,加上該部門每一個存活、同公司房間。它以部門為範圍,而不是以成員身分為範圍:部門房間不管呼叫者有沒有在裡面都算。搭配 `$me` 就構成兩筆 grant 的模型——業務用 department grant `{負責人 eq $me}`,主管用 per-user grant `{負責人 eq $me.department}`——而且兩個 token 可以放在同一個 `in` list,編譯成單一個對其聯集的 `in`。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/permissions
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{department_id}
新增複合指令

指令作者可自訂 agent 派送調解視窗的 TTL#

指令定義現在可以帶 `delivery_reconciliation_ttl_seconds`(60–604800,預設 900),並在每次執行時以絕對時間 `expires_at` 寫進派送標記,因此事後編輯定義不會回頭改動已提交標記的視窗。超過期限後標記會惰性到期:相同的 agent 呼叫不再被強制重播,也不再計入 16 個標記的調解容量,於是壞掉的確認路徑最多只能卡住一個 TTL,而不是永久卡死。不設定時採 omit-when-default 序列化,IaC diff 與 dependency contract 摘要都維持位元組相同。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
新增維運

兩個 root 端點可列出並釋放卡住的 agent 派送標記#

`GET /root/hotfix/custom-table-agent-deliveries` 列出 pending 或 expired 的標記,附上 `expires_at`、`in_window` 旗標、所屬指令與範圍,以及該列的存在時間。`POST …/acknowledge` 釋放指定的執行紀錄;`execute: false`(預設)只預覽逐筆結果、完全不寫入。要確認視窗仍然有效的標記必須加 `force: true`,因為那會取消仍在運作的 replay guard。兩個端點都不會回滾已提交的寫入。

  • GET /root/hotfix/custom-table-agent-deliveries
  • POST /root/hotfix/custom-table-agent-deliveries/acknowledge
新增AI 工具

受 command 治理的資料表拒絕原始 agent 寫入#

作者若把 `settings.agent_writes_via_commands_only` 設為 `true`,在跑著 command 通道的房間裡,只要該表是某個可見、已啟用 agent 的 command 的寫入目標,`custom_tables_insert_record`、`update_record`、`delete_record` 與 `bulk_record_actions` 就不再接受寫入,而是回 `{"error":"table_governed_by_commands"}` 並指向對應的 `custom_table_command_*` 工具;讀取、搜尋與分析不受影響。這道閘門在名稱解析之後、staged pin 之後才執行,因此 prepare、execute-staged 與 direct 三條通道都會經過它。列舉治理集合的動作 fail open,短暫的讀取錯誤不會變成整個房間的寫入中斷。

調整權限

Row policy 支援任意 `and` / `or` / `not` 布林樹#

`read_filter` / `edit_filter` 現在是一棵布林樹——`{"and": […]}`、`{"or": […]}`、`{"not": node}`,或一個 predicate leaf——不再限於扁平的 `and` list,上限為深度 5、每條 policy 24 個 predicate。既有儲存的 policy 位元組完全不變、判定結果也完全相同,因此沒有任何 migration。`{"not": {col eq X}}` 是嚴格補集,包含空白 cell,而 `{col neq X}` 仍然排除它們;驗證錯誤訊息現在會帶節點路徑(`row policy or[1].not …`)。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/permissions
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{department_id}
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/client-permissions/{client_id}
  • PATCH /private/module/custom_tables/{scope}/tables/{table_id}/default-permissions
  • PATCH /private/module/custom_tables/{scope}/tables/{table_id}/client-access
調整查詢

`principal` 欄位的 filter 與 row policy 運算元必須帶標籤#

未帶標籤的運算元——裸 id、讀取回來的加工 dict、IaC `$user:` token 或 `client:` actor key——在 REST stored query、舊版 `filters` map、agent 工具組的 query/aggregate/traverse 驗證器、SCP 純量葉節點、rollup 與 lookup 設定 filter,以及 grant row policy 上一律回 400。否則它會編譯成靜默零列的 `eq` 或比對每一列的 `neq`。`name_eq` 與 `name_contains` 在此型別上被拒絕:請先解析 principal,再用它的 `ref` 過濾。所有 principal 欄位仍然不可排序;`group_by` 以整個帶標籤字串為鍵。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/search
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/aggregate
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/permissions
調整權限

Acting room 會縮小 principal SET 的 room 腿#

在 agent session 或 compose command 內,acting room 會釘住兩個身分 SET 的 room 腿,優先序固定:`ScpDeny` 載體(trigger 執行、public callback 寫入)最優先且完全沒有 room 腿,其次是明確傳入的 acting room,再其次是已 stash 的 channel binding,都沒有時套用 REST 語意。被釘住的房間仍必須通過存活、公司錨點,以及 `$me` 的成員身分或 `$me.department` 的部門檢查,因此釘住永遠只會減少;keyword 與 binding 指向不同房間時,結果是沒有 room 腿加上一則 error log。

調整AI 工具

Direct 寫入通道才是文件上的預設值,拒絕文案也改為與通道無關#

在此來源 pin,`requires_confirmation_for_ct_action` 的 schema 與 `DEFAULT_CHAT_SETTINGS` 預設值都是 `false`;tool loader 中兩處把「設定不存在」讀成 `true` 的 fallback 現在都讀 `false`,因此從未寫過這個值的房間不再表現得像開啟了 proposal 輪。`confirmation_unavailable` 的客戶文案不再說「確認保護」——direct 通道的房間沒有這個對使用者可見的概念——改成「寫入保護暫時無法完成安全確認,因此這次沒有變更資料」。

修正查詢

CSV 與 XLSX 紀錄匯出回傳 200 但內容是空的#

請求日誌 middleware 用一個「每次呼叫都回應」的 stub 換掉了 `request._receive`,破壞了每個 `StreamingResponse` 併行執行的斷線監聽器,response task group 因此在標頭已送出後被取消。兩種匯出格式都回 200 但 0 bytes。該 stub 已移除;匯出現在會串流已渲染的檔案,principal cell 也渲染成可讀形式(`room:<name>`、`<platform>:<name>` 或使用者姓名)。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/export
修正AI 工具

分析 scope 不再把來源在 scope 外的計算欄位清成 null#

`custom_tables_analyze(table_ids=[…])` 是模型自己挑的委派聚焦範圍,因此它不再決定 rollup 或 lookup 的值能不能渲染:計算欄位的值授權與 `restricted` 標註改為查詢 principal 的完整即時權限表。deny-default 的形狀沒有變——讀取層級、row policy、SCP 底線與隱藏欄位照舊生效,真正讀不到的來源仍然是 null。Link 展開、host targeting、名稱解析、join 與 traversal 維持在分析 scope 內,`computed_filters` 與計算欄位的 `sort_column` 也是。

修正維運

自訂資料表的節流不再毒化共用的 Redis Cluster client#

`execute()` 之後呼叫一次 `ClusterPipeline.close()`,會把共用 cluster client 的 default node 清成 null,於是該行程之後每一個 pipeline 都失敗,直到 pod 重啟為止——包含 staged 寫入提案、確認狀態與各種快取。這個 close 已從 passphrase 節流(每 client 每表 300 秒 5 次)與名稱解析節流(內部每 300 秒 30 次、外部 10 次)移除,並有回歸測試用「close() 之後每次 execute() 都壞掉」的假物件同時驅動兩個節流。

2026-08-27

5.8.1
4 破壞性2 新增1 調整1 修正

Schedule protocol V2(teamsync-backend PR #1058,以 origin/master HEAD 026132f2a8aecba48d2ea12da0bffbdc4ed69ef1 合併,VERSION 5.8.1,staging 已上線)。Schedule trigger 取得持久的 per-occurrence identity:date_column_reached 每個 (trigger, record, 正規化後的值) 只觸發一次,值改變時重新武裝;interval/daily 把錯過的視窗合併成一次補跑並且永不重疊;失敗的 schedule run 會消耗其 identity,並以同一個 run、依凍結的定義重試;在既有 id 下更改 schedule.type 會被拒絕。新增嚴格五欄位 cron schedule type,以及 moderator 專用、唯讀的 schedule preview 端點。遷移是 lazy 的,不需要任何維運步驟;唯一的部署規則仍是慣例的 migration-before-traffic。

破壞性觸發器

date_column_reached 改為每個值觸發一次,而不是每筆 record 永久一次#

觸發 identity 是 (trigger, record, 正規化後的日期值)。同一個值無論 run 結果如何最多觸發一次;未見過的新值會重新武裝該 record;改回已觸發過的值不會再觸發。舊規則下已觸發過的 record,其目前的值視為已消耗,只有真正的值改變才會觸發。「已觸發集合」現在是持久的 occurrence ledger,刪除 run log 不再重新武裝任何東西。

你要改什麼若流程依賴「修改到期日不會再次通知」,請加上排除已結案列的 when,因為日期改變後現在會再次觸發。若流程依賴「刪除 run 列讓 record 重新觸發」,請改為更改日期值。
  • POST /root/custom-tables/run-due-triggers
破壞性觸發器

interval/daily 合併錯過的視窗,且同一 trigger 不會同時跑兩個 run#

每個 clock trigger 都保有持久的視窗 cursor。停機後只會為最新一個到期視窗建立一次補跑,中間錯過的視窗不會重播。當該 trigger 的 run 仍在 pending 或 running 時,後續視窗不會被插入;run 進入終端狀態後,下一次 tick 最多插入一個。終端 failed 的 run 會消耗其視窗,tick 不會再重建它。在時段之後才建立的 daily trigger 仍會在當天觸發那個時段,與以前相同。

你要改什麼不要在 interval/daily 之上自建補跑佇列;長時間停機只會產生一個 run,而不是每個錯過視窗各一個。若緩慢的 action 不能拖延下一個視窗,請縮短 action 或拆分 trigger,因為仍在執行的 run 現在會佔住該 trigger。
  • POST /root/custom-tables/run-due-triggers
破壞性觸發器

重試 schedule run 會執行 run 建立當時凍結的 trigger 定義#

每個新的 schedule run 都會儲存建立它的正規化 trigger 定義。重試會重新執行該 snapshot,仍會重新驗證目前的 schema、權限與目標是否存在,因此修改或刪除 live trigger 只影響未來的 run。本版之前建立的 run 沒有 snapshot,仍依 live 定義重試。當同一 trigger 仍有另一個 run 在執行時,重試 interval/daily/cron run 會回 409。

你要改什麼若要改變失敗的 schedule run 重試時做的事,請修正下游原因後重試,或讓它維持 failed、由修正後的 trigger 建立未來的 run。修改 trigger 不再能修復既有的 failed run。
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/trigger-runs/{run_id}/retry
破壞性觸發器

在既有 trigger id 下更改 schedule.type 會被拒絕#

當保留的 trigger id 在 date_column_reached、interval、daily、cron 之間切換時,REST PUT 與 IaC apply 會回 400 schedule_kind_change_requires_new_trigger_id。修改 schedule 的其他欄位會保留 id:date schedule 改變會重設輪替 cursor 但保留已消耗的值;clock schedule 改變會從新的 anchor 重新起算視窗。

你要改什麼移除舊 trigger(或從 IaC 文件刪掉那一行),再以不帶 id 的方式撰寫新 kind;IaC 會重新 plan 成一個 delete 加一個 create。
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
新增觸發器

schedule.type "cron":嚴格五欄位 expression,可選 timezone#

{"type":"cron","expression":"0 9 * * 1-5","timezone":"Asia/Taipei"}。欄位為 minute、hour、day-of-month、month、day-of-week,接受數值、*、清單、範圍與步進;星期日為 0;兩個日期欄位同時受限時採 POSIX OR。秒、macro、名稱、DOW 7、?、L、W、# 都是 400。春季前移時不存在的分鐘永遠不觸發,秋季回撥時重複的分鐘只觸發一次。cron 是 recordless:when 會被拒絕,$row token 與以列為目標的 action 會驗證失敗,而且永遠不會補跑 authoring 之前已過去的分鐘。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
新增觸發器

GET .../triggers/schedule-preview 說明 schedule 為何觸發或沒觸發#

三種 scope 都有的 moderator 專用、唯讀診斷。它執行 scheduler 自己的 evaluator,對每個 schedule trigger 回傳一個 arm:date arm 帶有下一頁 1,000 列 cursor page 內互斥的計數(missing_schedule_value、invalid_schedule_value、already_fired、scheduled_for_future、when_filter_mismatch、due_now);interval/daily/cron arm 帶有 last_consumed_window_at、due_at、due_now 與 active_run;損毀的儲存 trigger 會成為帶穩定 error_codes 的 invalid arm,不會遮蔽其他 trigger。它永遠不會建立 run,也不回傳任何 record id 或值。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/triggers/schedule-preview
調整觸發器

record_limit 改為輪替的 cursor page,大表不再餓死新列#

每個 date_column_reached trigger 都保有自己的 cursor,依 created_at、id 排序。每次 tick 讀取 cursor 之後的 record_limit 筆實體 live 列(最多繞回一次),在該頁內判定已觸發的列,即使沒有到期也推進 cursor;整頁都沒有到期列時,同一次 tick 最多再往前推進 10 頁。因此每一筆 live 列都會被輪替到;最壞情況的偵測延遲是 ceil(live_records / (10 × record_limit)) 次 tick,而不是永遠不會。

  • POST /root/custom-tables/run-due-triggers
修正維運

清除被其他表的 exists/not_exists rule 指向的表不再失敗#

清除的 cascade 會剝除指向被刪表的跨表 rule,但那段程式引用了從未 import 的模組,因此任何這類清除都回 500 Database operation failed。import 已補上,cascade 可以完成;schedule state、trigger runs 與 occurrences 會在父列之前依明確順序移除。

  • POST /root/custom-tables/{table_id}/purge

2026-08-23

5.7.12
2 破壞性1 調整

這個 code-first checkpoint 將手冊同步到 teamsync-backend origin/master HEAD fec260e4ee78573fa1eaf6e857fca6a9bfc36e3d。該 commit 的 VERSION 檔仍是 5.7.12,那是 HEAD 上的檔案內容,不是發行版宣告。內容涵蓋 Custom Tables 在 chatroom、department、company 三種 scope 的全部 family。ETL tables 仍是另一個模組;本站只收錄附件上傳會重用的那一條 public multipart chunk 路徑。Agent 寫入改由聊天室設定 requires_confirmation_for_ct_action 決定(先 prepare 再 token-only execute,或鎖定的 direct policy);每一則 Custom Table 工具結果都帶 customer-interaction tip;每張表最多 50 個 trigger;generated trigger chain 有預算;live source 列超過 1,000 時 materialize_slots 會 fail closed。

破壞性AI 工具

Agent 寫入改為 prepare 後 token-execute,或在 direct policy 下鎖定執行#

聊天室設定 requires_confirmation_for_ct_action(從來不是 tool argument)決定 roster。房間把它設為 true 時,預設第一輪只暴露 custom_tables_prepare_*;自然語言 proposal 真正可見後,後續肯定答覆才暴露對應的 custom_tables_execute_staged_* 或 custom_table_command_execute_staged_action,schema 只有 {token}。客戶仍然不必輸入 32 位 hex token。旗標為 false 時只暴露直接 business-argument 工具,並在 READ COMMITTED、FOR UPDATE 與保留的 direct-policy fence 下立即執行。每一則 Custom Table ToolMessage 也以同一句 customer-interaction tip 結尾。這份契約取代 2026-08-18「同一工具、相同 business arguments、模型路徑無 token」的指引。

你要改什麼不要再用相同 business arguments 呼叫同一支異動工具兩次。開啟確認政策時,第一輪 prepare、顯示完整自然語言變更,之後的肯定答覆只呼叫 token-only execute 工具。若房間把 requires_confirmation_for_ct_action 設為 false,直接工具呼叫一次即可,不要期待 proposal 輪。永遠不要把 token、JSON 或 ids 顯示給客戶。
破壞性觸發器

live source 列超過 1,000 時 materialize_slots 會 fail closed#

每次 tick 仍最多插入 1,000 個時段列,插入截斷仍算成功。但 live 班表/覆寫列超過 1,000 時,現在會在插入任何時段之前失敗,錯誤為 materialize_slots source row limit exceeded; archive unused source rows or split the template table before retrying。每條 chain 的 generated child runs 預算也是 1,000。

你要改什麼封存不用的樣板列或拆表,讓 live 掃描維持在 1,000 列以內。不要再依賴較舊 source 列被靜默略過,也不要期望 source.filter 搆到掃描上限以外的列。
  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
調整觸發器

每張表最多 50 個 trigger;run 以原子方式認領 pending#

REST 與 IaC 的 MAX_TRIGGERS_PER_TABLE 現為 50(rules 仍是 10)。At-least-once worker 以原子 pending→running 更新認領 run,同時送達的重複會在任何 action 前退出。Chain 在 depth 3 與 1,000 generated runs 停止;record-write action 最多針對 100 列。Webhook URL 與 header 值仍受 2,000 字模板上限約束,header 最多 20 個,且禁止空白/冒號/CR/LF 名稱。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
  • GET /private/module/custom_tables/{scope}/tables/{table_id}/trigger-runs

2026-08-20

5.7.9
1 破壞性4 新增1 調整2 修正

這個 code-first checkpoint 是直接查核已合併的 runtime code、route mounts、schemas 與 tests 後,將手冊同步到 teamsync-backend origin/master 的 32f532089bd90bbaaaf7e49f921e53c2ec62372f(version 5.7.9),並未把 PR body 當成契約。內容包含 interval 欄位、排程規則、saved-view 版型、lookup fallback、原子性 publish session、Sandbox writeback、confirmation recovery、command dependency refresh、擴充 grants,以及 REST/IaC/OpenAPI parity。PR #980 另外釘住 exact-scope command closure、完整的 own-scope refresh 分組、caller-scope tenant gate、依序取得的 multi-scope locks,以及仍存在的 concurrent-authoring fast-path 視窗。

破壞性IaC

IaC table name 上限為 64 字元,伺服器也會公開完整 line contract#

IaC table name 上限現在與 ORM 一致,為 64 而不是 100 字元。Table moderators 可接受並匯出 raw user UUID 或 $user:<username>;$dept、$smc 與 $room token 在此仍不合法。三種 scope 都掛載 GET .../tables/iac/contract,讓 OpenAPI 帶出完整 discriminated IacLine union;apply 也完整記錄文件層級的 400、404、409 與 422。

你要改什麼在 plan/apply 前拒絕超過 64 字元的 table name、允許 moderators 使用 $user:<username>,並從 contract endpoint 或其 IacLine schema 產生 IaC editor,不要繼續維護手寫 allowlist。
  • GET /private/module/custom_tables/{scope}/tables/iac/contract
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
新增欄位型別

Schema 新增 interval,達到 18 種 column type 與 13 種 rule type#

interval 以 canonical {start,end} minute-datetime object 儲存,且 end 必須嚴格晚於 start;它支援 overlaps、contains_point、presence filters 與依 start time 排序,但不屬於 scalar、key、formula、lookup、rollup、grouping 或 toolkit-query lane。新的 count_limit rule 可限制同表或 matched foreign table 的符合列數;no_overlap 也可用 date-days/datetime-minutes 表示 min_gap,並能以單一 interval_column 取代分離的 start/end 欄。

新增查詢

Saved view 增加 matrix/timeline 版型;lookup 增加有界 defaults-table fallback#

Saved view 現在可為 list、matrix 或 timeline,並有精確的 kind-specific config gate;backend 會保存與重新驗證 layout,record read 維持一般 response shape,由 frontend 負責渲染。Cardinality-one lookup,或帶 pick 的 many lookup,可宣告一段 fallback:目標必須是同公司、相同或更廣 scope 的 table,以一到兩組 equality pair 配對後讀取 target column。Fallback 僅供 evaluation,絕不跨越 permission denial,也不能用於 computed filter 或 sort。其 page-wide distinct-probe budget 為 min(5000, max(50, page rows × fallback 欄位數));budget 耗盡時會記錄 warning,未執行的 probe 呈現 null。不存在的 primary 可取得 default,而被拒絕的 primary 維持 null,因此保留一個刻意接受的 one-bit residual signal;唯一 target 若已 soft-delete,則視為未連結。

新增維運

部門與公司資料表可用一次原子性 flip 發布 staged dataset#

Moderator 可為每張表開啟一個 24 小時 session,把最多 5,000 筆 create-shape rows 暫存在可安全重試的 seq 0 到 63 slot,再非同步 commit。Staged rows 沒有 read path,也不接受 caller 提供的 record id。Commit 會重新授權 owner、限制 optional replace set 並套用 ACL floor,最後在同一 transaction soft-delete 與 insert;at-least-once delivery 只會 replay 同一張 ticket,不會重複 flip。

新增AI 工具

聊天室與部門擁有的 Sandbox job 可取得 run-scoped Custom Table writeback credential#

Published task version 可宣告 output_policy {kind: custom_table_writeback, table_id, allowed_ops: create|create,update}。Chatroom-owned job 只能在精確的 owner room 鑄造;department-owned job 則可在 owning department 的 room 鑄造。只有互動式 JWT user 可鑄造,且 author 與 submitter 權限都會重查。Company-owned policy 會在 publish 時被拒絕;owner audience 以外的 borrowed run 則永遠不會鑄造。Secret 只注入該次 run,並在每個 terminal transition 撤銷。Agent submission 必須走明確兩輪確認;submit_sandbox_job 則拒絕任何帶 output_policy 的 version,因此 trigger inventory仍精確維持十種 action type。

調整權限

公司資料表補齊 department/client grant parity,public query 也公開 request schema#

Department grant 現在可用於 department 與 company table;direct client grant 及 passphrase client-access 設定則掛載於 chatroom、department、company 三種 scope。Public token query route 現在會在 OpenAPI 公開具封閉 grammar 的 PublicQueryRequest,而 raw request body 在 parsing 前仍受 16 KiB 上限約束。

修正複合指令

Schema、rule、reference 與 purge mutation 會更新 selected、原本 valid 的 commands#

支援的 dependency-changing mutation 現在會更新 selected、變更前仍 valid 的 commands,涵蓋 column/schema edit、rules、cross-table reference、purge 與 repair path。原本 stale 或同時被修改的 definition 絕不會復活。一般 mutation refresh 與 business change 同 commit、採 best-effort,無法 compile 的 command 保持 stale;trigger replacement 仍保有更強的 atomic veto 與 rollback contract。Command dependency contract 採 exact-scope-closed:table 本身可以合法向上 link 或套用 rule,但 command 的 transitive closure 若觸及另一個 scope,authoring 會以 `A referenced table does not exist in this scope` 失敗。因此 multi-table purge/repair 可依 rewritten table 自己的 scope 分組,同時讓每個 participating scope 都收到完整 mutated-table set。Caller 與 table scope 不符時是 inert tenant gate,不做 command probe 或 lock;multi-scope prepare 則依 company → department → chatroom 順序鎖定參與的 roots。仍存在的 lock-free no-dependent fast path,代表 concurrent authoring 仍可能依 mutation 前形狀編譯並落成 stale。

修正AI 工具

Affirmative turn 不再卡死於 expired 或 completed mutation confirmation#

若 affirmative 抵達時 proposal 已遺失、過期、取消或從未 visible,伺服器會重新呈現一份 fresh inert PREPARED proposal,該回合完全不寫入。相同 affirmative 在 mutation 完成後重送只會回 already_completed,不會再次執行;若要刻意重複同一 business change,使用者必須明確重述,再確認新的 proposal。

2026-08-18

4 破壞性2 新增4 調整

這個僅代表 source 的 checkpoint,把 2026-08-17 的 merged-source 批次與 PR #955 整併為同一份最新紀錄。它以伺服器擁有的自然語言與 durable-delivery 契約取代模型可見的 Custom Table confirmation token、維持 realtime voice 唯讀、把受信任的 agent command delivery 綁定到其 channel,並原子性更新 trigger-dependent commands,搭配明確的 IaC drift 與 lock-conflict code;同時保留前一批的 bounded Analyst toolkit、逐表 staged-change 額度,以及 sandbox-job/attachment trigger actions。以下項目只描述已合併的後端 source,不代表 staging 已部署,也不代表真實 LINE/Meta delivery 已驗證。

破壞性AI 工具

Agent 異動確認改為自然語言、由伺服器擁有並感知 delivery#

這個項目取代 2026-08-17 的 token migration,而目前的整合指引已由 2026-08-23 的 agent-staged-token-execute-and-direct-policy 取代。歷史上,insert、update、delete、bulk 或 write-command 的第一次呼叫只送精確 business arguments,且完全不寫入。伺服器只渲染一份有界的自然語言 proposal;該 proposal 確實 durable-visible 後,後續明確且無歧義的確認才以相同 business arguments 呼叫同一工具,並由伺服器解析綁定 tenant、conversation、channel、actor 與 group 的 proposal。使用者可見訊息永遠不帶 proposal/receipt JSON、內部或結構性 UUID、伺服器持有的 digest、confirmation reference、token 或 tool lifecycle 資料;但外觀為 UUID/digest 的 business value 仍可能作為一般資料顯示。異動與 durable receipt 在同一 transaction 提交;回應或 delivery acknowledgement 遺失時會 reconcile 原本的執行,不會鑄出第二個副作用。

你要改什麼這份 migration 是歷史紀錄。目前整合請看 agent-staged-token-execute-and-direct-policy。在 2026-08-18:從所有 model/tool schema、持久化助理訊息與 retry 路徑移除 confirmation_token 與 confirmation_reference。Pending confirmation 時只顯示完整的自然語言 business change 並停止。絕不可要求使用者保存、轉送伺服器持有的 authority digest/key;結果不確定時先讀回現況,不可盲目重送異動。
破壞性AI 工具

Direct realtime voice 的 Custom Tables 改為唯讀#

OpenAI 與 ElevenLabs direct realtime session 保留 instructions、Analyst reads、record lookup、link-schema lookup 與已授權的 attachment viewing;row insert/update/delete/bulk、attachment write-input discovery、permission/passphrase mutation,以及整個 Custom Table Commands roster 都會被隱藏,因為直接串流的模型音訊沒有伺服器可驗證的 later-turn confirmation boundary。一般文字聊天仍保留自然語言兩輪寫入流程。

你要改什麼Direct realtime voice session 中要求的每一項 Custom Table 異動,都要引導回一般文字聊天。Realtime 中缺少 mutation 或 permission tool 時,不可推論為 full access。
破壞性複合指令

受信任的 agent command execute 綁定 channel 與 LINE group scope#

隱藏的 server-operator execute route 現在必須帶 X-TeamSync-Agent-Channel。X-TeamSync-Agent-Group-Scope 是 64 字元小寫 SHA-256 digest,只有 line_group 與 line_room 必須提供,其他 channel 反而不可提供。這些由伺服器注入的值會與 principal identity 一起進入 idempotency 與 pending-delivery context;header 是否存在與 channel 不符時回 400 invalid_agent_group_scope,缺少或格式錯誤的 typed header 則是 request-validation error。

你要改什麼只更新受信任的 server-side command adapter:一律注入已驗證 channel,且只在 channel 為 line_group 或 line_room 時注入伺服器推導的 group digest。絕不可從模型文字或 browser user 收集任一值。
  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
破壞性AI 工具

中繼的 token-based confirmation checkpoint 已被取代#

這個永久 anchor 記錄 2026-08-17 merged-source 的中繼契約,但不是目前的整合指引。PR #955 已從模型可見的 pending-confirmation result 移除 opaque confirmation reference/token 與由模型轉送的 command digest;模型仍會收到經 allow-list、無 token 的 proposal/instruction ToolMessage,而使用者可見回覆則強制為有界自然語言。那份 2026-08-18 流程本身已由 2026-08-23 的 agent-staged-token-execute-and-direct-policy 取代。

你要改什麼不要實作這個中繼 token 流程,也不要實作 2026-08-18 的同一工具確認路徑。請遵循 agent-staged-token-execute-and-direct-policy:先 prepare、顯示完整且有界的 business proposal,後續肯定答覆只呼叫 token-only execute 工具;若 requires_confirmation_for_ct_action 為 false,則使用鎖定的直接工具。
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
新增觸發器

第十種 trigger action 可提交已授權的 sandbox task version#

submit_sandbox_job 只適用於 created 與 updated 事件。設定會釘住已發布且啟用、無需確認的 task version、同公司 live chatroom、有上限的 timeout,以及從不可變事件 snapshot 解析 $row 樣板的 JSON-object input。觸發時會重新載入造成事件的 principal,並在建立 run 前重查 room、task、table、row、hidden column 與 channel-scope 權限。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
新增觸發器

send_channel_message 可傳送附件欄,並以可續傳的 part receipt 記錄進度#

Created/updated trigger 可指定最多五個不重複的附件來源欄,並從事件 snapshot 解析最多二十個同公司 blob。外部 channel 會把每個 blob 當成獨立 part;平台與內容型別支援時使用 provider-native media/file 訊息,只有 fallback 或不支援的形式才改送含永久公開 blob URL 的文字。Agent delivery 則把文字與 blobs 一起保存。每個完成的 part 都有 receipt,因此重試只接續未完成部分,不會重送已完成的 part。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
調整觸發器

Trigger 編輯會原子性更新原本 valid 的 dependent commands#

REST trigger PUT 與 IaC trigger apply 會鎖住 command scope 與 table,挑出編輯前 stored contract 仍然 valid 的 dependent commands,安裝候選 trigger set,並在同一 transaction 重新編譯那些 commands。原本已 stale 的 command 仍保持 stale,而且每條失敗分支都會回滾該行 bundle。在 REST 上,locked-graph 重新驗證或非資料庫的 compile/refresh failure 是結構化 409 command_dependency_refresh_failed;MySQL 1205/1213 是另一種 flat-detail 409,其他 database operational failure 則是淨化後的 flat-detail 500。成功 refresh 也會因 definition digest 改變,使編輯前尚未完成的 agent command confirmation 失效。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/triggers
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
調整IaC

IaC 明確分類 trigger plan drift 與可重試 lock 衝突#

IacLineError.code 現在接受 iac_plan_stale 或 retryable_lock_conflict。Trigger 的 before-fingerprint 在 plan 與 apply 之間改變時,會回 iac_plan_stale,detail 為 trigger changed after planning; run plan again。Per-line 或 line-0 maintenance boundary 遇到 MySQL 1205/1213 時回 retryable_lock_conflict;其他行可能已經執行,因此必須取得並審查 fresh plan 後才能再次 apply。同一文件中的 command fingerprint 會使用伺服器 refreshed dependency digest,不會只因前面的 trigger 行更新它而被誤判 stale。

  • POST /private/module/custom_tables/{scope}/tables/iac/apply
調整AI 工具

Toolkit V2 讓探索與分析具備界限、明確範圍、evidence 與即時授權#

custom_tables_get_instructions 現在是可分頁的 roster,可用 table_ids 或 name_contains 聚焦;custom_tables_analyze 接受有上限的明確資料表集合;custom_tables_lookup_link_schema 只回安全組合寫入所需的兩個 opaque link 欄位。分析結果以 ok/partial/denied/failed 搭配 evidence、warnings 與 allow-list reason codes 回傳。每次會讀取資料或指向特定資料表的頂層呼叫,都會重新整理帳號、tenant、room 與 table roster;委派操作在執行時還會再次檢查 row、column、ACL 與 channel-scope 權限。靜態的 custom_tables_cheat_sheet 不會開資料庫 session,也不會授予任何權限。

調整維運

Staged-change actor 額度改為每位 actor、每張表 500 筆,不再是跨表共用 10 筆#

某張繁忙資料表上的待審工作,不再吃掉同一 actor 在其他資料表的額度。獨立的全表 pending-create 額度仍是 50;staged_cap_exceeded 也仍會回結構化 scope 與 limit,client 應直接呈現這些欄位,不要硬編碼任一數字。

2026-08-02

6 破壞性14 新增9 調整11 修正

Acting room 從「你指名的東西」變成「你本來就是的東西」。REST 現在把每一張受治理的部門表,從「授權你這張表的所有房間」聯集解析——acting_chatroom_id 參數、它的 400 重試階梯與 403 雙生錯誤全部刪除,還在送這個參數的 client 會被靜默忽略。周邊還有:部門表有了自己的 department-permissions 介面與兩條不帶 scope 的探索路由、command 多了 agent_enabled 開關並且不再允許不引用任何資料表、agent toolkit 在讀取面追平 REST 並補上寫入,以及第十二種規則型別——在資料列符合前像條件時鎖住欄位。

破壞性權限

?acting_chatroom_id 從所有 REST 路由消失——現在是無聲忽略,不是拒絕#

五十七個 custom-table 操作移除了 acting_chatroom_id query 參數:36 個部門的 table/record/view/history/staged-changes/import 操作、15 個 blob 與 attachment 操作(5 條路由分別掛在裸的 /chatroom、/department、/company 三個掛載點下),以及 6 個 command execute/query 操作。帶 candidates 陣列的 400 acting_room_required 階梯、403 acting_room_unsatisfiable 交集檢查、兩個公司層的 422 守衛,以及 403 "This table is readable and writable only through a chatroom channel; you hold no chatroom grant on it.",全部連同它們的 response schema 一起刪除。還在送這個參數的 client 會拿到 200 與一模一樣的 body——FastAPI 會丟掉未知的 query 參數,所以它指名的房間既不會被拒絕、也不會被採用,而且沒有任何訊號告訴你它已經失效。

你要改什麼不要再送 acting_chatroom_id;它會被忽略,而答案現在是你被授權的所有房間的聯集。刪掉所有 acting_room_required 的重試分支與房間選擇器——沒有 candidates 清單,也沒有房間可挑。以前沒有房間的呼叫者會拿到那個大聲的 403,現在 list/search/aggregate 是 200 加零筆、listing 是 record_count: 0、單筆 GET 是統一的 404;只有寫入還會出聲,回 403 scp_scope_undeclared。
  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/records
  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/records/{record_id}
  • POST /private/module/custom_tables/department/tables/{table_id}/blobs/multipart/init
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/query
破壞性複合指令

手動 command execute/query 不再綁定 acting room——acting_room_unsatisfiable 整族刪除#

POST /{scopeWithId}/commands/{command_id}/execute 與 /query 在三個 scope 上都不再接受 acting room。跨表的 set.intersection 候選房間交集已經移除,所以 403 {"error": "acting_room_unsatisfiable", "tables": [...]} 與 400 {"error": "acting_room_required", "candidates": [...]} 都不可能再發生,acting_room_unsatisfiable 也從 execute 的錯誤 union 中拿掉。指令計畫觸及的每一張 channel 受管資料表,改為各自從呼叫者身上解析自己的 floor,所以呼叫者搆不到的那一張表,會以它自己的寫入 floor 403 scp_scope_undeclared 或 scp_out_of_scope 浮現。受信任的 agent 通道仍由伺服器從已驗證的 service principal 推導房間。

你要改什麼刪掉 command 通道裡所有依 acting_room_unsatisfiable 或 acting_room_required 分支的處理,也不用再為了「找出一間能滿足所有受管資料表的房間」而改寫指令結構。改為依逐表的 scp_scope_undeclared/scp_out_of_scope 拒絕來分支,並預期同一個指令走 agent 時觸及的資料列會比走 REST 少。
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/query
破壞性複合指令

引用不到任何資料表的 command 被拒絕,存量的零表 command 全面隱形#

編寫一個編譯後相依集合為空的 command,現在在建立與更新兩條路徑上都回 422 {"error": "command_references_no_table", "message": "a command must reference at least one custom table in this scope"}。空的引用表集合讓 command 介面上每一道權限閘門都變成恆真:任何租戶使用者都能寫一個只有 let 或只有 callback 的 command,然後讀它、改它、刪它、執行它、讀它的執行快照,並透過它的 callback 帶著呼叫者身分把資料送出去。這些閘門現在全部 fail closed,所以閘門之前存下的資料列不會出現在任何列表,而且在 GET、PUT、execute、query 與兩條 executions 路由上對所有呼叫者(包含公司層管理者)都回 404。

你要改什麼殭屍資料列仍然佔著它的名字。DELETE .../commands/{command_id} 仍然刪得掉——manage 探測失敗後改用 command 自身 scope 的權限(聊天室建立者或管理者、部門管理者、公司管理者)——IaC apply 的 delete arm 也可以,它在閘門之前就 return。PUT 修不好這種資料列,請刪掉重建。IaC apply 的 create 與 update arm 現在會以逐行錯誤 command '<ref>' references no table: a command must operate on at least one custom table in this scope 拒絕同樣的形狀。
  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
破壞性複合指令

省略的 optional input 不能再放寬 match 條件,也不能藏在 $case 裡#

在 v1 定義裡,match 條件內綁不到值的 optional input 現在會讓執行失敗,回 400 step '<name>': optional input cannot omit a match key。把該鍵丟掉在 data 上是對的(表的預設值會接手),但在 match 上等於無聲放寬了 update/delete 的資料列選取條件,讓破壞性寫入打到比作者預期更多的資料列;where 本來就是 fail closed。另外,$case 裡任何位置省略 optional——某個分支的 then,或是 else——都會回 400 step '<name>': optional input cannot omit a $case result,data 與 match 兩種脈絡皆然。分支選擇是稍後對著鎖定的 pre-image 才發生,所以「這個分支綁不起來」必須是錯誤而不是猜測:先前的做法是在未被選中的分支綁不起來時把整個鍵丟掉,欄位停在舊值,執行卻仍回報 succeeded。

你要改什麼依賴舊有放寬行為的存量指令,現在會在執行時回 400。請把每個 match key 綁到必填 input,或依形狀拆成多個指令。$case 裡每個分支的 then 與 else 都必須綁得到值——可能缺席的 optional 根本不能出現在那裡。
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
破壞性複合指令

讀不到的 select 來源或 join 目標回 403,不再是 200 加一個空關聯#

v2 program 的 select 來源與每一個 join 目標,現在都和它的寫入目標一樣嚴格地受權限管制:讀不到的那一張回 403 Read access not granted for this table.。先前 row-ACL 層會把 can_read: "none" 編成一個永不匹配的條件,於是 /query 回 200 加一個空關聯、agent 答「no matching records」;在寫入型 program 裡,先讀後寫的去重步驟看不到符合的資料列,就插入了一筆重複資料。這道閘門刻意比相依契約窄:只用來 insert 的目標表可以合理地是 can_read: "none" 搭配 can_insert: true,而被相依契約拉進來的 link 或 lookup 表在讀不到時仍然渲染成只有 id 的 stub。

你要改什麼為 program 會 select 或 join 的每一張表授予讀取權限。以前默默依賴「空關聯」的 program——去重探測、存在性檢查——現在會大聲失敗而不是寫錯資料,因此過去回 200 的執行會改以 403 浮現。
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/query
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/agent/commands/{command_id}/query
  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
破壞性權限

agent 與 trigger 的指令通道把部門表權限釘在所在聊天室#

v1 plan 與 v2 program 的權限解析器現在會接收 acting room,並把它轉交給 resolve_effective_permissions。在受信任的 agent 與 trigger 通道上,部門層資料表的授權被釘在所在聊天室、當成硬天花板,與 agent 的表格工具一致:以前綁在某間房的 agent 指令會折疊呼叫者所屬所有房間的 REST 聯集,等於可以借用另一間房的授權,動到這間房從未被授權的資料。手動 REST 明確不帶 acting room,維持以使用者為中心的聯集,所以同一個指令走 REST 合理地會比走 agent 觸及更多資料列。

你要改什麼agent 或 trigger 指令若動到「所在聊天室沒有授權」的部門表,現在會回 403 Insert access not granted for this table./Edit access not granted for this table./Read access not granted for this table.。請直接把該表授權給那間聊天室,不要再依賴呼叫者的其他房間。
  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
  • POST /private/module/custom_tables/agent/commands/{command_id}/query
新增權限

部門表有了自己的 department-permissions 端點——部門授權以前只有 IaC 寫得進去#

把某個部門的存取權授予另一個部門的資料表,以前完全沒有 REST 端點。聊天室那組端點用 chatroom id 綁定資料表,而部門層級的表 chatroom_id IS NULL,所以唯一的寫入途徑是 principal.type 為 "department" 的 IaC grant line,也沒有任何介面列得出這些授權列。現在部門 scope 下多了四條路由——列表,以及以 target_department_id 定位的授權、更新與撤銷(叫這個名字是因為 {department_id} 已經是資料表自己的 scope)——全部由 table manager 依賴把關。POST 會解析目標部門;目標必須存在於 caller 的公司,否則回 404 Department not found。PATCH 會先要求 grant row 存在(404 Department permission not found),再執行相同的 company-anchored target validation;因此 missing-grant 的 404 precedence 保持不變,而已移出該公司的 target 仍會被拒絕。read_filter、edit_filter 與 visible_columns 會被接受、寫入,resolver 也真的照做,但 DepartmentPermissionResponse 沒有這些欄位——列表永遠不會回吐它們。

  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/department-permissions
  • POST /private/module/custom_tables/department/{department_id}/tables/{table_id}/department-permissions/{target_department_id}
  • PATCH /private/module/custom_tables/department/{department_id}/tables/{table_id}/department-permissions/{target_department_id}
  • DELETE /private/module/custom_tables/department/{department_id}/tables/{table_id}/department-permissions/{target_department_id}
新增權限

settings.default_permissions.audience:"company" 讓部門表對全公司每個部門開放#

default_permissions 以前只對 in-scope 的主體生效,所以要把部門表擴大到整間公司,就得為每個部門各建一列部門授權。現在 payload 收 audience——"scope"(預設)或 "company"——在部門表上填 "company" 會讓同公司的每位使用者都算 in-scope,並同時取得通過 membership door 的鑰匙。其他地方一律拒絕:default-permissions 通道(只有 PATCH,沒有 PUT)回 422 settings.default_permissions.audience "company" is only valid on department-scoped tables;建表通道收的是原始 settings dict,回 400,訊息是同一句或 settings.default_permissions.audience must be "scope" or "company", got '<value>'。只有 "company" 這個值會被存下來,所以之後一次沒帶 audience 的 PATCH 會無聲地把分享收回。

  • PATCH /private/module/custom_tables/{scope}/tables/{table_id}/default-permissions
  • POST /private/module/custom_tables/{scope}/tables
新增權限

shared-with-me 列出從你自己 scope 之外分享給你的資料表#

所有列表路由都掛在 scope 之下,被授權者又無從得知是哪個部門或哪間聊天室分享給自己,於是在呼叫任何 by-id 路由之前,table id 只能靠人工遞過來。這條不帶 scope 的新路由會列出可透過四種授權之一抵達的活資料表——個人授權、給呼叫者所屬部門的授權、透過自己所屬的活聊天室取得的 internal 授權,或帶 audience:"company" 的部門表——每一筆都標出 sources(依字母排序),以及 resolver 算出的 my_permissions。skip 與 limit 分頁(limit 上限 200,scope 列表則是 1000),total 是分頁前的存活筆數。缺口是刻意留的:它是互補而非涵蓋,所以自己部門的表、公司層級的表、自己所屬房間的表都不會出現;沒有建立者/moderator 這一臂,跨部門 moderator 拿 id 進得去卻永遠不會被列出;而 can_read:"none" 的拒絕列會讓資料表在這裡隱形,儘管 membership door 仍然放行。

  • GET /private/module/custom_tables/shared-with-me
新增查詢

用完全相同的名稱解析出 table id,橫跨公司與部門 scope#

get_current_user 就是全部的把關:沒有角色檢查,路徑上也沒有 scope 段。查詢橫跨呼叫者公司裡的公司層級與部門層級資料表,回傳 resolver 判定 can_read != "none" 的那一筆,附上 scope、department_id、department_name 與 my_permissions。比對是逐位元組精確的——name 欄位是 utf8mb4_bin,所以 registry 與 Regist 都比不中名為 Registry 的表——而 uq_custom_table_name_per_company 同時涵蓋兩種 scope,最多只會有一筆命中,因此沒有 tie-break、也沒有優先順序。所有落空都是同一顆 404 {"detail": "Table not found"}:不存在、讀不到、在垃圾桶、別家公司的、聊天室層級的,看起來一模一樣,不會開出存在性 oracle。它之所以存在,是因為公司列表路由是 role>=3 的硬閘門,而 shared-with-me 又刻意排除公司層級與自己部門的表——一張讀得到的表,它的 id 可能不出現在呼叫者搆得到的任何列表上,這也是前端以前逐環境寫死 id 的原因。

  • GET /private/module/custom_tables/tables/resolve
新增維運

immutable_when 是第十二種 rule:以資料列的 pre-image 鎖住欄位#

文法是 {"type": "immutable_when", "name"?, "prior_when": [≤5 個 predicate], "columns": [≤20 個欄位]},而 prior_when 是對資料列的 PRE-image 求值——這補上了其他機制補不了的兩個洞。transition 規則看不見一次讓 Status 維持 posted、卻偷改 Qty 的寫入;而其他所有 rule 的 when 都跑在 post-image 上,於是 {"Status": "draft", "Qty": 6} 在同一個請求裡解鎖又改值,條件從頭到尾沒有命中過。它不收一般的 when(送了就是設定期 400),只鎖 inline 儲存的欄位,也只在 update 與 revert 上執行。違反時回 400,detail 是一整串 Rule '<name>' violated: 'Qty', 'Price' cannot change while the row matches the lock condition (prior_when evaluates the row as it was before this write)——所有被改動的鎖定欄位會用逗號併進同一句訊息,請不要去 parse 它。在掛有相符 require_approval 規則的表上,這道鎖在 stage 階段不會否決(寫入在 rules engine 之前就先被 stage),而簽核 apply 的重播會特地跳過 immutable_when,所以這道鎖的意思是「沒人能靜悄悄改掉它」,不是「永遠不能改」。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/rules
新增維運

同步的 records/bulk 收 expected_version——不符就整批回滾#

每個 entry 只能在 action:"update" 上帶 expected_version(整數 ≥ 1);insert 或 delete 帶了就是 422(expected_version is only valid on update actions — an insert has no prior version and delete carries no data to guard)。這是最後一條完全沒有版本護欄的寫入通道:model 禁止未知的 key,所以以前送這個欄位會 422,而批次裡一筆過期的 update 會無聲蓋掉併發的贏家。因為這批是原子的,答案不是 2026-07-28 release 記載的非同步通道那種逐筆跳過——而是 409 {"error": "version_conflict", "message": "Action i: record was modified concurrently (current version X, expected Y); the atomic batch was rolled back — re-read and retry.", "action_index", "record_id", "current_version", "expected_version"},而且所有動作一起回滾。在掛有相符 require_approval 規則的表上,原子呼叫者拿到的是另一顆 409 {"error": "record_changed_during_staging", "message": "a target record changed while the batch was being staged; nothing was staged or executed — retry", "record_ids": [...]},沒有 action_index、也沒有版本號,所以別只認 version_conflict。同一批裡較早的動作若動到同一列會先把 version 加上去,後面帶護欄的動作就會 fail closed。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk
新增複合指令

agent_enabled 把 command 從 AI 手上收走,REST 完全不受影響#

存下來的 command 以前沒有對 AI 的可見性開關:只要通過 tag 與資料表讀取權檢查,就會被載入成 agent 工具,想讓 AI 碰不到它只能刪掉或拆掉 tag 綁定。現在建立與更新都收 agent_enabled(布林,預設 true),會被寫入,並在每個 command 回應裡回吐——單筆 GET、列表項目,以及建立/更新/刪除的回應。設成 false 會把它從工具清單移除,而受信任的 agent 通道每次呼叫都會重新檢查這個旗標,因此在一個進行中的 session 裡早已綁定的工具會 fail closed,回 404 Command not found,與不存在的 id 逐位元組相同——這條通道不洩漏存在性訊號。REST 毫無變化:它仍然列得到、取得到、執行得了、查詢得了,旗標照實回傳 false。PUT 是整份取代,所以一次沒帶這個 key 的更新會把它重新開啟。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
新增複合指令

outputs[].description 以 Outputs: 區塊進入 agent 的工具描述#

scalar 與 relation 兩種 output 形式都收一個選填的 description,上限 1024 字元。在此之前 definition.outputs 根本不會送到模型面前——output 名稱只以 key 的形式出現在執行/查詢的回應 body 裡,AI 只能從名字猜它是什麼意思。現在每個宣告的 output 都會被附加到 agent 工具描述的 Outputs: 區塊,一個 output 一行:- <name>: <description>,沒寫描述時就是單純的 - <name>。這個欄位預設是 null、未設定就省略,所以既有的定義與 IaC 文件都能逐位元組穩定往返;要注意的是,明確寫成空字串會被當成沒有,不會留進編譯後的儲存內容。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
新增複合指令

truncated 標示出重播的是被 audit 上限裁掉的快照#

當一次大型執行的回應快照被裁到 65,536 位元組的 audit 上限時,裁切標記只存在於儲存的快照裡。同 key 的冪等重播會透過一個沒有這個欄位的 model 重建回應,旗標因而被丟掉:呼叫者拿到的 body 少了宣告的 outputs 與 step 資料列,卻沒有任何辦法把它跟「真的沒有結果」區分開。現在 CommandExecutionResponse 帶有 truncated,只在 execute 通道的重播路徑上出現——那條路徑會原封不動回傳儲存的 body。執行歷史那兩條路由是用固定的欄位清單組出 body 的,不含這個欄位,所以那裡永遠不會出現——在那些路由上,裁切標記只留在巢狀的 response_body 裡。欄位為 null 時會省略,所以沒被裁切的執行回應與從前完全相同。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
新增AI 工具

Agent 工具箱在相對日期、計算欄位與全文搜尋上追平 REST#

query_records 與 aggregate_records 現在可以在 date/datetime 欄位以及 created_at/updated_at 上使用 within_last、within_next 與 older_than,邊界由伺服器時鐘透過 REST 編譯器自己的 helper 算出,而不是另寫一份。query_records 另外拿到 computed_filters——rollup、cardinality 為 one 的 lookup,以及單純算術/比較的 formula predicate,最多 3 條,與 filters、any_of、q 以 AND 結合——可用計算欄位的 sort_column,還有上限 200 字元的全域 q 文字搜尋,對所有可見的 string/text 欄位以 OR 比對。兩項計算欄位能力在活資料列超過 50,000 筆時一律拒絕,而且這道護欄是在任何 predicate 編譯之前先讀資料表的活列數,所以就算用一般 filters 收窄也解不開——儘管拒絕訊息聽起來像可以。IF/AND/OR 的 formula 仍然不能篩選也不能排序。

新增AI 工具

AI 拿到了 upsert、樂觀鎖,以及 50 個動作的原子批次#

custom_tables_insert_record 收 match_column:恰好命中一列就在 row lock 下更新那一列,沒命中就插入,命中多列則拒絕這種語意不明的 upsert,回應會帶 "upsert":"updated" | "created",更新分支還會附上 version。「dora 在就更新、不在就新增」以前是先找再寫的兩段式操作,中間有競態窗口,找不到時就多長出一列重複資料。custom_tables_update_record 收 expected_version(≥ 1),在 row lock 下驗證,不符時回 {"error": "version_conflict", "current_version", "expected_version"},不再覆蓋掉 AI 從沒看過的那次編輯;在 2026-08-02 當時,不帶這個參數就維持 last-writer-wins。PR #955 之後收窄了 agent write 的這句話:省略它仍讓「較早讀取→提案」窗口沒有 guard,但確認層一定會釘住提案當下的 record version,並拒絕後續 drift。新工具 custom_tables_bulk_record_actions 一次套用最多 50 個 insert/update/delete 動作,全有或全無,只註冊給同時具備 insert 與 edit 的內部主體——批次只帶 changed_by、沒有客戶身分,外部客戶的批次會讓建立者歸屬無處可放——而 own 與 filtered 範圍的編輯者,每一列都會重跑一次編輯權限檢查。

新增IaC

command line 收 agent_enabled——可 diff、apply 會寫入、預設值不會出現在 export#

IaC 的 command spec 多了 agent_enabled(布林,預設 true),文件因此可以發佈一個在 REST、IaC 與 triggers 上完全可呼叫、卻被排除在所有 agent 端查詢之外的 command。它和 name、description 一樣參與 desired/last-applied/live 的三方欄位比對,所以在文件裡翻動它會 plan 出恰好一次 command update,changes[] 會點名這個欄位;而在 IaC 之外用 REST 翻動它,會出現在 drifted_fields。Export 採預設即省略——只有被隱藏的 command 才會寫出 "agent_enabled": false——所以在這個欄位存在之前撰寫的系統,匯出結果仍逐位元組穩定,早於此欄位寫下的 state row 也會重新 plan 成 noop、drifted_fields 為空。陷阱就是同一條省略規則反過來讀:重放一份較舊的文件,會無聲地把某人用 REST 隱藏起來的 command 重新曝光。

  • POST /private/module/custom_tables/{scope}/tables/iac/plan
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
  • GET /private/module/custom_tables/{scope}/tables/iac/export
新增IaC

IaC 拒絕在部門 scope 之外使用 default_permissions.audience:"company"#

table line 的 spec.settings 以前是逐個 key 併進線上資料表、完全不驗 default_permissions.audience,而逐行解析器沒有 scope 上下文——所以在聊天室層級的文件裡寫下 "company",會被原樣存成一段看起來像全公司分享、resolver 卻不理會的 settings。現在每個非 absent 的 table line 都會在 validate 階段對照文件自身的 scope 檢查:違反時是該行的驗證錯誤,訊息為 settings.default_permissions.audience "company" is only valid on department-scoped tables(或 settings.default_permissions.audience must be "scope" or "company", got '<value>'),plan 會回報 applyable: false。executor 在套用 settings 變更時會對線上資料表自己的 scope 再檢查一次,所以直接 apply,或拿已 stale、不再與 live state 相符的 plan 來 apply,也繞不過去;在那裡它是該行的 apply 錯誤。這個 key 會經由 export 完整往返,所以名正言順帶著它的部門文件重新 plan 會是 noop。

  • POST /private/module/custom_tables/{scope}/tables/iac/plan
  • POST /private/module/custom_tables/{scope}/tables/iac/apply
調整權限

在強制的 channel SCP 下,REST 解析的是你被授權的所有聊天室的聯集#

REST 的每一條 custom-table 通道現在都只帶著呼叫者本人,並逐表解析 channel floor:該表的管理者豁免,否則 scope 是呼叫者所屬、且對該表持有 internal 授權的每一間「活著的」聊天室的 scope_values 聯集,聯集為空即拒絕。同時在 A、B 兩房的成員一次請求就讀到 A ∪ B——以前多房成員會直接解析成拒絕、什麼都看不到。拒絕在讀取端是沉默的:list/search/aggregate 回 200 零筆、單筆 GET 回統一的 404、列表的 record_count 為 0。拒絕在寫入端則有代碼:聯集為空時回 403 {"error": "scp_scope_undeclared", "message": "no chatroom of yours declares a scope on this table"},聯集非空但不涵蓋目標時回 scp_out_of_scope。scope_values 為 null 或空的房間不貢獻任何值,軟刪除的房間會退出聯集,而聯集可以合法地超過 MAX_SCOPE_VALUES 的 200 個值——那始終只是單一授權列的撰寫上限。

  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/records
  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/records/{record_id}
  • POST /private/module/custom_tables/department/{department_id}/tables/{table_id}/records
調整權限

權限解析第 4 步改為合併所有聊天室授權,不再排名後只留一列#

第 4 步以前會把呼叫者的 internal 授權列排名後只留一列,同分時先比 granted_at、再比 chatroom_id——於是透過 A 房拿到北區、B 房拿到南區的使用者只看得到其中一半,而決定權來自他觀察不到的 tie-break。REST 現在逐軸摺疊所有符合資格的列:can_read/can_edit 取最高等級,平手時 filtered 確定性地優先於 own;can_insert 取 OR;有貢獻的 filtered row filter 則合併——只有一個相異 filter 時原樣沿用,多個時包成 {"or": [arms]},相同的 filter 會去重回到裸的單一 arm。這個聯集形狀只在解析時產生:從不被撰寫、從不被儲存、從不進入 agent 通道,而且執行時逐 arm fail-closed,某一房為空或帶未解析 token 的 filter 不會把另一房合法授權的資料列一起遮掉。撰寫端的文法完全沒變——所有寫入介面仍然只接受 {"and": [predicates]},送出 {"or": …} 依舊回 'row policy must be an object of the form {"and": [ predicates ]}'。agent 通道仍然釘住 session 的房間,不做任何摺疊。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/permissions/me
  • GET /private/module/custom_tables/shared-with-me
調整權限

多房摺疊後的 visible_columns 可能比任何單一房間授權的還窄#

摺疊時會為每一列有貢獻的授權算出一個 coverage key——(can_read, read_filter, can_edit, edit_filter, can_insert)。當所有有貢獻的列 coverage 完全相同,欄位白名單取聯集,而其中只要有一列沒有白名單就整個解除上限,因為「沒有」代表不受限。當 coverage 不同——filter arm 相異,或等級混雜——已宣告的白名單改成取交集,沒有白名單的那一列視為全集而退出交集。理由是列×欄的相關性:只有 A 房授權的那些資料列上,不該冒出 B 房才有的欄位;而且同一份白名單也管住寫入,這裡給多了同時是讀取洩漏與寫入提權。少給是刻意選擇的安全近似,所以在兩間 row filter 不同的房間裡的使用者,最後可能看到比任何一房單獨宣告的還少的欄位。

  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/records
  • PUT /private/module/custom_tables/department/{department_id}/tables/{table_id}/records/{record_id}
調整AI 工具

同一個身分在 REST 與 AI session 裡得到不同答案,這是刻意的#

REST 回答的是「你是誰」——你被授權的所有房間的聯集。agent toolkit 回答的是「這個 session 在哪一間房」:它在建立時蓋上一次該聊天室的 binding,之後每一張**部門層**資料表都只用那一房的授權列解析,既是釘選也是天花板;那一房沒有授權列就是無權存取,不管是誰在問、表管理者也一樣,結果再逐軸與使用者自身權限取交集。聊天室層與公司層的表沒有聊天室授權列,不受這道天花板限制。兩個介面現在刻意不對稱,而且沒有任何參數能重現 agent 的切片——已經沒有參數可以釘房間了。同一個身分同時在 room_north 與 room_south,透過 HTTP 列出 4 筆、在 room_north 的 session 裡只有 2 筆:「API 顯示 4 筆,機器人卻說 2 筆」是預期行為,不是缺陷。

  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/records
調整權限

新增資料表 moderator 改以資料表的有效公司為錨點,不再看路徑上的 scope#

目標使用者現在在每個 scope 都改用 resolve_table_company_id(table) 解析出的有效公司驗證,而不是路徑上的 scope id,所以同公司但不在擁有該表的部門裡的使用者,也能被指派為部門層資料表的 moderator——以前那是硬性的 404「User not found or not in this department」。這個家族只掛在部門與公司兩個 scope,兩句 per-scope 訊息收斂成同一句 404 {"detail": "User not found or not in the same company"}——真正改掉的只有部門那一句,所以只有比對部門字串的客戶端會看到訊息變了。它 fail-closed:有效公司解不出來時整段查詢直接跳過並丟出同一顆 404,誰都加不進去。沒有變的部分:路由仍然掛在 CustomTableModeratorRequired 上,重複新增仍是 400「User is already a moderator」,而移除完全不帶成員資格條件——跨部門或 IaC 產生的 moderator 列都移除得掉。

  • POST /private/module/custom_tables/department/{department_id}/tables/{table_id}/moderators/{user_id}
  • POST /private/module/custom_tables/company/tables/{table_id}/moderators/{user_id}
調整複合指令

execute/query 的 400/409/422 除了原本的區域 union 也宣告 CommandProgramError envelope#

POST .../commands/{command_id}/execute 的 400、409、422 現在宣告 anyOf[<原本的區域 union>, CommandProgramErrorResponse],/query 則是 400 與 409——它的 422 本來就是這個 envelope。線上行為沒有改變:compose executor 一直都會用這些狀態碼丟出 CommandProgramError,只是形狀從未被文件化,照著 spec 產生的 client 解不開 {"detail": {"error", "phase", "step", "path", "message", "retryable", "details"}}。請以 detail.error 分流,絕對不要以狀態碼分流——同一個 409 既可能是區域 union 的 approval_required,也可能是 envelope 的 command_lock_conflict。details 一定存在(可能是 {}),step 與 path 則經常缺席,phase 是 author|execute|stage|release|callback 其中之一。隱藏的 agent 通道沒有跟著放寬。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/query
調整複合指令

受信任的 /agent/commands 通道離開了 openapi.json,但仍然註冊、仍然服務#

agent_router 現在帶著 include_in_schema=False,所以 POST /private/module/custom_tables/agent/commands/{command_id}/execute 與 /query 不再出現在 openapi.json 與發布的 snapshot 裡。隱藏不等於移除:兩條路由仍然解析得到、仍然對外服務。它們用 CUSTOM_TABLE_COMMAND_SERVICE_TOKEN 這個共享密鑰,加上伺服器注入的 X-TeamSync-User-ID/X-TeamSync-Chatroom-ID/X-TeamSync-Social-Media-Client-ID 身分標頭驗證,而不是使用者 JWT;把它們掛在裸的 module tag 下、參數寫成一般 header、又完全沒有 security scheme,等於誤報了它的驗證契約。這也是唯一還會釘 acting room 的 command 通道,且由已驗證的服務主體在伺服器端決定——手動的 REST 路由一個房間都不綁。

  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
  • POST /private/module/custom_tables/agent/commands/{command_id}/query
調整複合指令

Command 與 input 的 description 上限提高到 1024 字元#

compose command 的 description 從 512 字元、每個宣告 input 的 description 從 256 字元,一起提高到 1024,REST 的 request/response model 與 IaC 的 command spec 都一樣。舊上限太緊,不足以向 agent 與 REST 呼叫者說明這個 command 到底在做什麼——而 command 的 description 正是最後變成 agent 工具描述的那段字。超長仍然是標準的 422。

調整查詢

PUT records/order 回 400 並點名 slot,不再是原始的 uq_table_sort_order 409#

reorder 的衝突預檢以前只查活的資料列,於是被軟刪除資料列佔住的 sort_order 會通過探測,寫入到 flush 才炸成 409「Duplicate value '<table_id>-<slot>' violates unique constraint 'uq_table_sort_order'.」——一個資料庫形狀的答案,既沒點名請求裡出問題的那個 slot,也沒給出空的 slot,而且無從查起,因為被刪掉的資料列在每個讀取介面都是隱形的。探測現在涵蓋所有資料列,所以這一類輸入會在任何寫入落地之前撞上既有的 400「sort_order values [...] already used by other records in this table」。這個 400 字串本身不是新的;新的是會走到它的輸入類別。探測之上的資料列存在性檢查刻意仍然只看活的、在 scope 內的資料列——reorder 的兩趟 bulk UPDATE 不會觸發資料列寫入 floor,那道 SELECT 是唯一的 scope 守門。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/records/order
修正權限

部門授權終於打得開部門層級資料表的成員門#

部門層級資料表 by-id 介面前的那道成員門以前只認兩把鑰匙——使用者本人的 grant 列,以及透過自己所屬的活聊天室取得的 internal 聊天室 grant——而權限解析器一直都認部門列。於是「部門有授權、人不在該部門」的使用者,授權列在、解析得出真實層級,每一條讀取與資料列路由卻都回 403 {"detail": "Insufficient permissions for department-scoped table."}:解析器放行、門口擋下。現在這道門多了一支以 user.department_id 為鍵的部門臂。權限仍由解析器決定:can_read: "none" 的部門列會開門,然後在讀取時用它自己那顆不同的 403 擋下;角色達到 CAN_MANAGE_COMPANY 的呼叫者本來就繞過這道門,不受影響。

  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}
  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/records
  • POST /private/module/custom_tables/department/{department_id}/tables/{table_id}/records
修正權限

明確的 moderator 列同樣打得開部門資料表那道門#

同一類「解析器放行、門口擋下」的問題,只是高一層:users_custom_tables_association 裡的明確列會讓解析器直接短路成 is_manager: true,但那道門根本不認識它。角色低於 CAN_MANAGE_COMPANY 的跨部門 moderator——現在能從 REST 指派,過去也一直能用 IaC 造出來——在整個 by-id 介面上都拿到 403 {"detail": "Insufficient permissions for department-scoped table."}:資料表詳細、資料列、views、欄位、匯出、staged changes 全部。現在 moderator 列本身就是一把鑰匙。角色達到 CAN_MANAGE_COMPANY 的跨部門 moderator 從來沒被擋過,行為不變。

  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}
  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}/records
修正權限

跨公司的殘留 moderator 列不再能授權 schema、rule、trigger 或授權寫入#

管理者 dependency 一找到明確的 moderator 關聯列就立刻返回,部門/公司的租戶錨點只跑在後面的預設管理者路徑上。於是一個被設為資料表 moderator、之後調到別家公司的使用者,仍然握有該表的 schema、rules、triggers、授權、預設權限與欄位 ACL 寫入權——掛在這個 dependency 上的每一條路由都繼承了這個洞。現在有效公司(走 resolve_table_company_id 解析,而不是可為 NULL 的 CustomTable.company_id 原值)會在 explicit-moderator 捷徑之前先錨定,並回 403 {"detail": "Insufficient permissions. (Different Company)"}。同公司的 moderator 不受影響;有效公司完全解析不出來的資料表仍然通過這一項檢查。

  • PUT /private/module/custom_tables/{scope}/tables/{table_id}/rules
  • PATCH /private/module/custom_tables/{scope}/tables/{table_id}/default-permissions
  • PATCH /private/module/custom_tables/{scope}/tables/{table_id}/column-acl
修正查詢

record_count 改為呼叫者的聯集,部門資料表詳細頁也不再回 400#

在受治理的部門資料表上,清單通道對任何有一個以上候選聊天室的人都解析成 deny,於是部門資料表清單對呼叫者明明讀得到的資料表回報 record_count: 0——而同一個人、同一張表,詳細頁卻回 400 acting_room_required。現在兩條通道放的是同一個載體,並且逐表以相同方式解析:該表的管理者豁免、拿到完整筆數;否則就以呼叫者所有被授權聊天室範圍的聯集來計數;只有在完全沒有被授權的聊天室時才是 0——與被遮蔽的詳細讀取一致。這個數字仍然不會超過同一個呼叫者在詳細端點看得到的內容,而詳細頁回 200。

  • GET /private/module/custom_tables/department/{department_id}/tables
  • GET /private/module/custom_tables/department/{department_id}/tables/{table_id}
修正查詢

殘留的 sort_order 位置不再讓每一次寫入都撞上 uq_table_sort_order 409#

uq_table_sort_order 的唯一性是 (table_id, sort_order),範圍涵蓋整張資料列表、包含已刪除的列,但每一處「下一個位置」的計算都只看未刪除的列。在刪除開始把位置設為 NULL 之前就被軟刪的列——或被 migration 原樣搬過來的列——仍然佔著一個位置,於是只看活列的 MAX 讀少了,寫入監聽器發出一個已被佔用的位置,寫入就死在 409 "Duplicate value '<table_id>-<slot>' violates unique constraint 'uq_table_sort_order'.",而且是該表的每一次寫入都死,是必然而非偶發;現場的樣子是一個新開的部門一筆資料都建不起來。現在五處「下一個位置」的計算都涵蓋與約束相同的資料列範圍:寫入監聽器、records/bulk、records/bulk-insert、測試資料寫入,以及 restore 的 max+1 後備。健康的資料表完全不受影響——每條刪除通道都會把 sort_order 設為 NULL,而 MAX() 會忽略 NULL。保證的範圍是刻意窄的:被佔用的位置不會再被重複發出,但刪除仍然會把那個整數釋放給之後的寫入重用。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk-insert
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/{record_id}/restore
修正維運

非同步 bulk-update 的單筆錯誤帶的是拒絕內容,不是框架的 repr#

背景工作對每一筆只捕捉裸的 Exception,於是以 HTTPException 失敗的資料列——SCP 拒絕就是 HTTPException——被 str(e) 渲染成 FastAPI 的 repr:一個以 "403: {'error': 'scp_out_of_scope', ...}" 開頭的字串,狀態碼被黏在最前面,而且與 upsert 迴圈對同一種拒絕寫出的內容不一致。現在會先捕捉 HTTPException 分支,只附上 detail 內容,與 upsert 迴圈一致;任何在解析那個字串、或在剝掉 "403: " 前綴的 client 都必須修改。它在 errors[] 裡仍然是 Python repr 字串、不是結構化物件,而以裸 Exception 失敗的資料列沒有變。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk-update
修正維運

一筆壞掉的 command plan 不再讓整頁 staged-changes 回 500#

每一筆 command_plan 的 staged 列都會為 moderator 渲染,而格式壞掉或屬於舊版的 payload——沒有 frozen_program 的第 2 版 payload、plan 不是清單的第 1 版 payload——會在渲染器裡丟出一個裸 ValueError。它沒有被接住,所以一筆壞列就讓整個清單請求以 500 收場:moderator 看不到、因此也無法丟棄該表上的任何 staged change。現在渲染被包起來了。清單回 200,健康的列保留原本的 payload,壞掉那一列的 payload 變成 {"error": "unrenderable_staged_payload", "message": "This staged command plan is malformed or predates the current payload format and cannot be rendered; it can still be discarded."}——該列仍然看得到、也仍然丟得掉。分頁、排序與逐呼叫者的可見性規則都沒有變。

  • GET /private/module/custom_tables/{scope}/tables/{table_id}/staged-changes
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/staged-changes/discard
修正複合指令

核准計畫釋出時遇到鎖錯誤會重新排隊,不再把計畫丟掉#

釋出已核准的第 1 版計畫時,任何例外都被當成終局,於是一次暫時性的 1205 鎖等待逾時或 1213 死結,就永久丟掉一份人類已經核准的計畫。現在它會 rollback、把執行留在 staged,並在一般的 apply 嘗試預算下把 staged change 重新排回簽核掃描;原因記成正規化 JSON 信封 {"details":{},"error":"command_execution_failed","message":"Approved command release should be retried","phase":"release","retryable":true}。另外,只要不可重試的失敗在 orig/__cause__ 鏈上任何一層帶有 SQLAlchemy 成因,原因就會被換成安全字串:過去原始的 [SQL: …] [parameters: {…}] 驅動文字會被寫進 staged_change.error 與客戶端讀得到的執行快照,而那些綁定參數可能包含凍結計畫時被重新注入、ACL 本來藏起來的欄位值。

  • GET /private/module/custom_tables/{scopeWithId}/commands/{command_id}/executions
  • GET /private/module/custom_tables/{scopeWithId}/commands/{command_id}/executions/{execution_id}
修正複合指令

已提交的 succeeded 或 staged 執行不會再被改寫成 failed#

執行路徑在同一個 try 裡先 commit、再 refresh 執行列。如果 commit 其實成功了但回應遺失,或是 commit 之後的 refresh 丟出例外,控制流就掉進失敗分支,把已提交的列覆寫成 failed——這會毀掉 response_body 裡的重播快照,於是客戶端用同一把 key 的冪等重試會重新執行指令,而不是重播已存下的終局結果。現在失敗標記會重新載入該列,若狀態已是 succeeded 或 staged 就原封不動回傳;commit 之後的 refresh 也搬出 commit 的 try、放進自己的區塊,失敗只記錄不擴散。呼叫端仍然拿到原本那個錯誤。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
修正IaC

IaC parse 會實際量測 JSON 巢狀深度——超過 128 層括號的行回 400#

唯一的巢狀防護是包在 json.loads 外面的 except RecursionError,而現代 CPython 根本不會觸發它,於是一顆巢狀炸彈會被完整解析、通過 schema 驗證、深拷貝並排進 plan,才輪得到任何更窄的上限。現在 parse 會自己數括號巢狀,逐一實體 JSONL 行、在 json.loads 之前就數:上限硬寫成 128,不是 CUSTOM_TABLE_IAC_MAX_* 那組可調參數;量測範圍是整行、包含行本身的外層信封;掃描會辨識字串字面值與跳脫字元;判斷是嚴格大於——128 通過,129 中止並回 line {n}: excessively nested JSON structure (max depth 128)。它是每行的第一項檢查,所以在過深的行上會搶在 invalid JSON、header 必須是首行且唯一的規則,以及 record/line/table 上限之前;只有文件層級的位元組上限與 UTF-8 解碼仍跑在更前面。原始 malformed apply 會在 dispatch 前同步被拒絕。若文件已被非同步通道接受,之後在 worker 強制 reparse 時才失敗,poll body 會以 body.status: "failed" 與 body.error.code: "parse_error" 回報同一段訊息。

  • POST /private/module/custom_tables/{scope}/tables/iac/plan
  • POST /private/module/custom_tables/{scope}/tables/iac/apply

2026-07-28

2 新增4 調整6 修正

永久刪除成為租戶端的正式兩步驟操作;一批「靜默給錯答案」的缺陷改為大聲失敗。資料表與整個 tag 系統可以走 preview–ticket 同意流程 purge,非同步批次更新支援樂觀鎖,未知的 request key 全面改回 422 而不是被丟掉,而對系統 id 欄位的篩選終於真的比對得到資料列。

新增維運

租戶 purge:preview → 一次性 ticket → 核准執行,單表或整個 tag 系統#

永久刪除不再需要 root 操作員。preview 會算出完整爆炸半徑——資料列、外部表會被剝除的連結欄位與 rules、saved views、IaC state rows、將被刪除或變殭屍的 commands——並發出綁定呼叫者與當下結構、效期 15 分鐘的一次性 ticket。execute 無論成敗都燒掉 ticket;preview 之後任何結構漂移會回 409 purge_preview_stale。tag lane 一次銷毀所有成員表、該 tag 的 commands(名字立即釋放)、其 IaC state rows 與 tag 本身;成員表掛著別的 tag 是 fail-closed blocker。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/purge/preview
  • POST /private/module/custom_tables/{scope}/tables/{table_id}/purge
  • POST /private/module/custom_tables/{scope}/table-tags/{tag_id}/purge/preview
  • POST /private/module/custom_tables/{scope}/table-tags/{tag_id}/purge
新增維運

非同步 bulk-update 的 item 支援 expected_version,在 row lock 下比對#

每個 update item 可以帶 expected_version(整數 ≥ 1),語意鏡射單筆 PUT。不符的 item 會被跳過,並在輪詢的 ticket 裡回報:status 為 completed_with_errors,errors[] 內含 {"error": "version_conflict", "expected_version", "current_version"},error_count 精確,逐筆明細上限 100 筆。提交回應在結構上仍是 200 + ticket——非同步通道不可能回 per-item 的 HTTP 409。沒帶欄位的 item 維持 last-writer-wins;bulk-delete 不收這個欄位。簽核閘門下,版本衝突的資料列不會被 stage,HELD 批次在 apply 時洗不掉這道 guard。

  • POST /private/module/custom_tables/{scope}/tables/{table_id}/records/bulk-update
調整維運

未知的 request key 全面改回 422,不再無聲丟掉騙你成功#

十五個 request model 補上 extra="forbid":aggregate 請求與其 metrics、四個 bulk payload 及其內層的單筆 action/update item model、search body(expand_links 屬於 query string、不是 body)、saved view 的建立與更新、export、欄位建立與更新,以及 view config。這些以前全都回 200、同時無聲忽略未知 key——對 aggregate 送 stored_filters 等於整個查詢沒有過濾就跑完。已存在、帶著雜 key 的 view 仍然讀得到(讀取側寬鬆剝除未知 key);IaC 的 view apply 則逐行驗證 key。

調整權限

設定裡引用 id 的存量 config 從無效變成生效#

同一個幽靈 id 缺陷也藏在存量設定裡:rules 的 exists/not-exists where 條件、rollup 與計算欄位的 filter、row policy、upsert 的 match-by-id,以前全部無聲比對不到任何東西。現在它們對準真正的主鍵——rules 開始擋寫入、row policy 開始給出可見範圍、upsert match-by-id 真的會比中而不是無聲插入重複列。請盤點任何提到 id 的 rule、policy、rollup filter 或 upsert 設定:你從未見過的行為現在會被強制執行。撰寫端則在 id 撐不起語意的地方直接拒絕:SCP 的 scalar leaf 與 no_overlap 的 start/end/date 欄位。

調整查詢

非正規的日期時間篩選邊界回 400,不再是悄悄偏移的邊界#

日期與日期時間儲存格是正規字串(YYYY-MM-DD、YYYY-MM-DD HH:MM)、以字典序比較,所以帶秒數——或帶斜線——的邊界會讓 eq/lte/gte 悄悄偏移。現在邊界跟其他通道一樣先驗證:in 逐項驗(而且必須是清單)、between 驗兩端與個數。存了非正規邊界的 saved view 或 IaC spec,讀取時會拿到 400 而不是悄悄偏移的結果;請修正存下來的邊界。真正的 created_at/updated_at 邊界不變,那裡的秒數仍然合法。

調整維運

鎖等待逾時第一次發生就回可重試的 409#

伺服器行程內的重試現在只涵蓋死結(1213)。鎖等待逾時(1205)在浮出水面前已經把資料庫的整個等待窗口堵滿,行程內再重跑等於把那段等待放大約三倍——超過多數 client 與 proxy 的時限。它現在第一次發生就回同一個可重試的 409。你的 client 該做的事完全不變:兩種情況仍是暫時性、沒有任何部分資料落地、請重試的契約一字不差——只是伺服器不再霸著你的連線慢慢重試那個慢的。

修正查詢

對系統 id 欄位篩選現在真的找得到資料列#

客戶端真正拿在手上的 id 是資料列的主鍵,但查詢通道把 id 條件編譯到 data blob 裡的幽靈 uuid 上——於是 id 篩選回 200 卻永遠 0 筆、以 id 排序毫無意義、count::id 只是碰巧等於列數。現在所有讀取通道都對準真正的主鍵。id 支援不透明識別碼的運算子集合——eq、neq、in、is_null、is_not_null——排序或子字串運算子用在 id 上是 400;legacy filters dict 收精確 id(字串或清單);對 id 的 aggregate 指標只有 count/count_distinct。

修正權限

併發的 settings PATCH 不再無聲互相覆蓋#

以前每個 settings PATCH 都是整包 read-modify-write、完全沒有鎖:兩個寫入者改不同的 key,兩邊都拿到 200,晚 commit 的把早的那個 key 蓋回去——收緊成「僅管理者可見」的欄位 ACL 可能無聲退回全員可見。現在六個 scope 端點(三個 scope 的 default-permissions 與 column-acl)、聊天室 client-access 通行碼設定、IaC executor 的 settings 寫入,全部改在資料表列鎖下合併單一 key;schema 寫入端在同一把鎖下重讀,加欄位撞上 settings PATCH 也蓋不掉 column_mapping。代價:競爭時 PATCH 可能等鎖、然後回可重試的 409,而不是回一個假的 200。

修正維運

附件 complete-upload 遇到鎖衝突不再毀掉整個上傳#

以前在寫入 blob 資料列時遇到暫時性鎖衝突,會直接走進補償路徑:已完整上傳的物件被刪掉、session 被銷毀、呼叫端拿到 500。現在補償前先辨識衝突——物件與 session(24 小時 TTL)都保留,回應是標準的可重試 409,拿同一個 session 重呼 complete 就能把上傳收尾。complete 同時具備冪等性:儲存層已經組裝完物件時,重試會確認並回傳 blob,而不是拿著已被消耗的 upload id 再失敗一次。

修正複合指令

表刪了之後留下的 command 刪得掉了,而且刪掉的 command 會釋放名字#

刪除一個表已經先被刪掉的 command 以前會失敗——權限檢查試圖解析那張已不存在的活表。現在兩條刪除路徑(REST 與 IaC)在表不在時改用 command 自身的 scope 判斷權限;沒有權限的呼叫者看到的仍是同一顆不可枚舉的 404。另外,command 名稱唯一性現在只計活列:刪掉的 command 名字立即可以重用。更新仍要求活表——PUT 是整份重編譯,殭屍更新沒有意義。

修正AI 工具

AI session 讀部門表用的是所在聊天室的授權,不是你最好那間房的#

同時在 A、B 兩間房的使用者,以前可以在 A 房的 AI session 裡用 B 房比較寬的授權讀部門表——而回答會渲染進共享聊天室,等於把資料列、欄位甚至整張表洩漏給從未被授權的聽眾。現在 agent toolkit 解析部門表時,所在聊天室的授權列既是釘選也是天花板:該房沒有授權列就是無權存取,不管是誰在問、表管理者也一樣;結果再逐軸與使用者自身權限取交集,個人較窄的限制仍然生效。REST 端點、聊天室/公司層級的表、commands toolkit 都沒有變。

修正權限

直接授權外部客戶現在會同時結案他的申請、並主動告訴他#

從成員權限畫面直接授權,以前只寫入權限,申請單永遠掛在待審、客戶也沒有任何通知——對話裡最後一句話停在「請等待審核」,agent 就一直拒絕客戶其實已經有權做的事。現在管理者授權會同時結案該客戶的待審申請,並推播訊息說明他現在能做什麼。同一次追查一併修好:LINE 群組/聊天室客戶以前根本送不出申請、新開房間的申請送達零個審核者、部門表的申請完全跳過管理者通知,以及拒絕無聲——現在會把審核者的 review note 一起送給申請人。

2026-07-27

1 新增1 調整4 修正

「撞在一起」從伺服器錯誤變成有文件的正常結果。在資料庫層相撞的寫入會由伺服器替你重試,真的傳到呼叫端的那一種衝突是有型別、可重試的 409,排程觸發器動作不會再因為一次衝突就把工作丟掉,而錯誤內容也不再夾帶 SQL 語句與參數。

新增複合指令

command_lock_conflict:競爭到的指令是可重試,不是壞掉#

執行通道會用資料列鎖把授權房間序列化,所以併發呼叫本來就會競爭。這種情況以前會變成 500 command_execution_failed、retryable 為 false,等於告訴呼叫端「這個指令壞了」。現在它回 409 command_lock_conflict、retryable 為 true,而且同一個冪等鍵可以立刻重用,因為失敗路徑會釋放預約。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
調整維運

寫入在鎖競爭中落敗會回 409「請重試」,不再是 500#

MySQL 死結(1213)與鎖等待逾時(1205)被視為暫時性狀況。擁有自己 transaction 的寫入路徑會以抖動退避重試三次,所以大多數競爭根本不會傳到你這邊。三次都輸時,請求會回 409 並附上「Concurrent write conflict (lock); please retry the request.」,而且沒有任何一半的資料落地。涵蓋的路徑包含建立資料表、欄位異動、資料列寫入、批次 sort-order、授權寫入與觸發器執行器。

修正觸發器

排程動作不會再因為一次衝突就把工作丟掉#

排程觸發器動作只要遇到一次暫時性鎖衝突,run 就會在第一次嘗試被記成失敗,而業務動作被無聲丟掉;在每日排程上,那天的工作等於沒做。現在動作會在 run 內重試,有上限也有抖動。它之所以安全,理由和續跑相同:進度逐 action 保存,已完成的 receipt 會依穩定 action id 跳過。

修正複合指令

AI 通道現在真的跑得動公司層的指令#

所有透過 AI 工具呼叫的公司層 v2 指令都會失敗,回 422「acting_chatroom_id is not applicable to a company-scoped table」:路由本來信任呼叫端,但重新準備存取時把這個判斷弄丟了。現在這個信任跟著已準備好的存取一起走,同時也修好了「AI 暫存的公司層指令通過核准後放行」以及「觸發器呼叫公司層指令」這兩條路。

  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
修正權限

競爭中的授權寫入不再回一個光禿禿的 500#

授權寫入路徑完全沒有鎖處理,所以它在取列鎖時遇到死結,呼叫端拿到的是 {"detail":"Internal Server Error"}:沒有重試提示,連清理過的訊息都沒有。現在它和其他寫入路徑一樣會重試,重試用盡則回可重試的 409。

修正維運

錯誤內容不再夾帶 SQL 語句與參數#

驅動錯誤轉成字串時會帶出 SQL 語句與其參數,而有三條面向客戶端的路徑原樣送出,包含客戶端會輪詢的批次工單 error_message。現在客戶端看得到的訊息會在單一收斂點清理:暫時性衝突變成 please-retry 的 409,其他都變成「Database operation failed.」,細節留在伺服器日誌。

2026-07-25

4 破壞性16 新增5 調整2 修正

這是本站建立以來最大的一次更新。複合指令帶來跨表交易寫入,新增三種欄位型別(json、user、social_client),資料表可以唯讀公開給匿名讀者,row policy 有了每次請求才解析的動態代號,觸發器動作從五種變九種,JSONL IaC 從九種行類型變十二種,而本手冊過去描述的那些部署開關都已刪除:這些能力不再需要「開啟」。其中四項會改變你既有的行為,請先看那四項。

破壞性權限

Row policy 不再匹配空白儲存格#

「filtered」授權在 SQL 端過濾資料列的邏輯已修正,與 Python 端一致。值運算子(eq、neq、範圍、in、contains)不再匹配空白儲存格;空白包含缺少該鍵、SQL NULL、明確的 JSON null,以及空字串。因此用 neq $me 寫成的「不是我的」條件,不再回傳負責人欄位從未填寫的資料列。

你要改什麼檢查所有 read_filter 或 edit_filter 用到 neq、範圍或 contains、且欄位可能為空的授權:這些使用者現在看到的資料列會變少。如果原本的意圖是「未指派的資料列也要看得到」,請改用 is_null 明確表達,不要依賴舊的巧合行為。
破壞性觸發器

觸發器的寫入改用觸發者的權限執行#

當使用者的寫入觸發了一次執行(created、updated、deleted、restored),該次執行的 create_record、update_record、delete_record、materialize_slots 動作會以那位使用者自己的 ACL 執行,而不再擁有完整系統權限。由排程觸發的執行仍保有系統權限。歸屬不變:資料列仍記為觸發器作者所建立。

你要改什麼逐一檢查會寫入其他資料表的觸發器:確認那些「會觸發它」的人對目標資料表真的有寫入權限。如果他們沒有、也不該有,就把工作改成排程觸發器,排程仍以系統權限執行。
破壞性權限

撤銷使用者授權會回傳撤銷後的權限#

三種範圍的撤銷端點都不再只回傳成功訊息,而是回傳 RevokePermissionResponse:使用者撤銷後的實際權限,加上 widened_access 與 warning,前後狀態在同一個鎖內解析。移除一個「收窄」用的授權,有可能反而讓使用者看到更多資料列,現在回應會直接告訴你,而不是讓你自己踩到。

你要改什麼如果你的用戶端對舊的成功回應做了型別綁定,請改成新的回應型別。接著要真的處理那個旗標:widened_access 為 true 表示這次撤銷讓該使用者的存取範圍變大,而不是變小。
破壞性欄位型別

附件的 MIME 與大小限制真的會擋下寫入#

allowed_mime_types 與 max_file_bytes 以前只是裝飾。現在上傳時是建議性檢查,寫入資料列時則是決定性檢查,且所有寫入路徑都適用;S3Object.size_bytes 會在 multipart complete 時實際量測,並出現在 AttachmentInfo 上。

你要改什麼檢查那些「先填上去再說」的附件欄位設定:以前混得過去的寫入現在會失敗。要嘛放寬欄位設定、要嘛修上傳端,並把被拒絕的原因顯示在介面上,不要盲目重試。
新增複合指令

複合指令:多張表的寫入放進同一個交易#

複合指令是一段儲存起來、有名字、有標籤、可帶參數的程式:它在同一個資料庫交易裡跨多張表寫入,並且用呼叫者自己的權限執行。三種範圍都有完整的 CRUD、執行端點與執行稽核紀錄,另外有一條給 AI 代理用的 service token 通道。執行是全有全無,冪等鍵依呼叫者分桶並可持久重播,而任何被引用的 schema 有變動時,整次執行會 fail closed,不會做一半。

  • POST /private/module/custom_tables/{scopeWithId}/commands
  • GET /private/module/custom_tables/{scopeWithId}/commands
  • PUT /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • DELETE /private/module/custom_tables/{scopeWithId}/commands/{command_id}
  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute
  • GET /private/module/custom_tables/{scopeWithId}/commands/{command_id}/executions
  • POST /private/module/custom_tables/agent/commands/{command_id}/execute
新增複合指令

查詢型指令:唯讀、可用 cursor 分頁#

版本 2 的指令若完全沒有寫入步驟,可以宣告 mode: "query",改由專屬路由呼叫:不上鎖、不寫稽核紀錄,用不透明 cursor 分頁,page.limit 上限 100。用 execute 呼叫查詢型指令,或用 query 呼叫寫入型指令,兩邊都會得到 409。

  • POST /private/module/custom_tables/{scopeWithId}/commands/{command_id}/query
新增欄位型別

json 欄位型別#

json 儲存格把一份任意結構的文件直接放在資料列裡,序列化上限 64 KiB、深度 32、節點 8192,任何深度出現非有限數值都會被拒絕。它刻意不是純量型別:彙總、查找、公式、unique 規則、row policy、group_by 與 CSV 匯入都會明確拒絕它。REST 的整格過濾支援 is_null/is_not_null,以及對純量值的 eq/neq/in,但不能排序 json;AI 工具則可以走 json path、算長度、依葉節點分組,還能剖析結構。

新增欄位型別

user 與 social_client 欄位型別#

`user` 或 `social_client` 欄位儲存一個原始 id:同租戶內的使用者,或聊天室屬於該租戶的社群客戶。一般資料列寫入只接受 id,不接受顯示名稱;IaC 資料列儲存格也接受相符的 `$user:` 或 `$smc:` 可攜式代號。讀取會將已儲存的 id 解析成顯示物件,解析不到的 id 就保留原字串。這兩種欄位只支援等值比較,且不可排序。這同時也讓本站舊的「沒有 user 欄位型別」說法正式失效。

新增權限

Row policy 動態代號:$me、$me.department、$today±Nd、$now#

授權的資料列過濾條件現在可以只寫一次、每次請求才解析。代號以原字串存在授權上,只在權限解析器那一層被替換,所以「我的資料列」或「未來七天」不再需要一人一條授權、也不用每天重寫。row policy 的值裡整個 $ 前綴都是保留字;查詢通道不會解析代號,貼到 stored_filters 裡它只是一個匹配不到任何東西的死字串。

新增公開讀取

把單一檢視唯讀公開給匿名讀者#

資料表管理者可以鑄造一個權杖,公開某張表的某一個已存檢視。權杖分兩種:無密鑰(URL 本身就是憑證),或帶 bearer secret(只在鑄造時顯示一次)。匿名讀者只會拿到 id、時間戳與 data,不會拿到 created_by、核准狀態或租戶 id;附件、連結、彙總、查找欄位則完全不能公開。未知、已撤銷或過期的權杖、權杖驗證失敗,以及無法存取的公開檢視共用相同的 404;超過流量限制回 429,呼叫者查詢輸入無效時也可能回 400 或 422。每個權杖各有自己的流量限制。

  • POST /private/module/custom_tables/{scopeName}/tables/{table_id}/public-read-tokens
  • GET /private/module/custom_tables/{scopeName}/tables/{table_id}/public-read-tokens
  • DELETE /private/module/custom_tables/{scopeName}/tables/{table_id}/public-read-tokens/{token_id}
  • GET /public/module/custom_tables/read/{token_id}/records
  • POST /public/module/custom_tables/read/{token_id}/query
新增公開讀取

公開表單可以先問出自己能寫哪些欄位#

公開回呼通道多了一個 form-schema 路由,用與寫入相同的驗證方式回傳目標資料表可寫入的欄位,失敗時同樣回統一的 404。外部表單現在可以直接照合約長出畫面,不必硬寫遲早會過期的欄位名稱。

  • GET /public/module/custom_tables/callback/{token_id}/form-schema
新增觸發器

四種新的觸發器動作#

觸發器原本有五種動作,現在有九種。send_channel_message 會依資料列解析出一位社群客戶並推送訊息,範圍涵蓋聊天室屬於該公司的任何客戶,並支援 LINE Flex。invoke_command 以觸發器作者的身分執行一個複合指令。delete_record 只軟刪除觸發它的那一列。materialize_slots 把班表資料列展開成可預約的時段。

新增觸發器

可預約時段改用產生,不再手動塞資料#

materialize_slots 掛在每日排程上,把「每週班表 + 例外」的資料列展開成同公司目標表裡實際可預約的時段列,滾動視窗最長 92 天,並可切成固定長度。它以自然鍵去重,且被軟刪除的時段仍算存在,所以取消掉的時段不會又悄悄長回來。

新增查詢

相對日期、空值判斷、OR 分組、多欄排序#

查詢條件新增 within_last、within_next、older_than,每次執行時都會解析成絕對區間;另外新增 is_empty 與 is_not_empty,這兩個也是附件欄位唯一支援的運算子。請求層新增 any_of(OR of AND,最多 10 組、每組 10 個條件)與 sort(最多三個排序鍵,並以資料列 id 決勝)。這四項在資料列、搜尋、匯出與已存檢視都可用,而規則、row policy 等「設定」介面則刻意拒絕它們。

新增查詢

一對多的查找可以挑一列,之後就像純量欄位#

一對多的查找欄位可以加上 pick(排序欄位與方向),把它收斂成勝出的那一列。因此它第一次可以被 computed filters 過濾、可以排序,也可以當公式的運算元:「最近一筆訂單的狀態」現在是一個欄位,而不是一個彙總加一堆繞路。

新增權限

SCP 可以只放寬讀取,不放寬寫入#

扁平的 channel 規則可以帶 read_op: "overlaps":只要有任一個連結目標落在目前聊天室的範圍內,該列就可讀,而寫入門檻仍要求完整包含。這個後果是刻意設計的,也值得你在設計時考慮:一列可以被列出來,儲存時卻被拒絕。若 require_present 不為 true,overlaps 會被拒絕。

新增IaC

三種新的 IaC 行類型:command、insight_selection、public_read#

JSONL 文件現在有十二種行類型,不是九種。複合指令、單一聊天室的洞察系統啟用、以及已公開的讀取權杖都能宣告,並和其他資源一樣走 plan/apply,狀態模型也追蹤這三種新資源。已公開的權杖可以就地更新,且憑證 URL 保持不變。

新增IaC

JSONL 文件可以在環境之間搬動#

可攜式身分代號($user:、$dept:、$smc:、$room:)可取代受支援的授權主體與洞察系統選擇參照中的環境專屬 id。IaC 的 `user` 資料列儲存格接受 `$user:`,`social_client` 儲存格接受 `$smc:`;`$dept:` 與 `$room:` 不是資料列儲存格值。規則與觸發器的設定支援跨環境的 $row 欄位引用。匯出會輸出受支援的代號,所以從測試環境搬到正式環境不必再手動替換一堆 uuid。資料表管理者也能宣告,語意是整組取代。

新增維運

三個維運掃描:指令與跨表引用#

維運端新增三個工具:依保留期限清理已完成的指令執行紀錄、重新排入卡住的指令回呼執行,以及在資料表自己的遷移鎖下修復損壞的公式跨表引用。修復工具有預覽模式,正式執行前先跑預覽。

  • POST /root/hotfix/custom-table-command-executions/prune
  • POST /root/hotfix/custom-table-command-callback-runs/requeue
  • POST /root/hotfix/custom-table-crossref-repair
新增觸發器

排程可以設定位移與時區#

「日期到達」排程可以設定 offset_minutes,範圍 -43200 到 43200:負值是提前提醒,正值是寬限期。每日排程可以設定 IANA 時區,所以「09:00」是營運所在地的早上九點。兩者在等於預設值時不會存入,所以文件裡明寫預設值仍然會 plan 成 noop。

調整維運

這個模組不再有任何部署開關#

過去看起來需要「部署時開啟」的九項能力現在一律開啟,對應的環境變數與關閉分支都已刪除:row policy 與欄位 ACL、聊天室授權、SCP、規則、計算欄位、JSONL IaC、核准閘門、AI job scope 拆分、資料表標籤。這些能力不再需要維運者開啟,也不再回各自的「此部署未啟用」拒絕;一般授權與設定檢查仍然適用。只剩兩個開關:一個是事故用的,可以把 SCP 強制切成只記錄不阻擋;另一個是檔案匯入時要不要用 LLM 命名欄位,預設關閉。

調整觸發器

動作有了穩定 id,重排順序不再打亂重試#

每個動作現在都有伺服器產生的 id,放在與 actions 平行的頂層 action_ids 清單裡,讀取時會回填到各個動作上。失敗續跑與各動作的冪等判斷都改用這個 id,而不是在清單中的位置,所以編輯或重排動作不會再讓做到一半的執行搞混。

調整IaC

改動線上計算欄位會直接是 plan 錯誤#

以前修改線上計算欄位的設定會被 plan 靜靜丟掉,作者看到的是「套用成功」但什麼都沒變。現在這是明確的 plan 錯誤,並會指出是哪個欄位。當 plan 已經跟現況不符時,apply 也會回傳穩定的逐行錯誤碼 iac_plan_stale。

調整AI 工具

AI 不再隨口說「這筆資料不存在」#

讀取回傳零列時,現在會附帶 absence 判定:not_found、not_visible 或 held,而 AI 只有在 not_found 時才可以說「這筆資料不存在」。「我看不到」和「它不存在」是兩件不同的事,而使用者以前收到的是自信的那一句。

調整查詢

沒有「日曆區間」運算子,這是決定,不是漏做#

在此來源 pin,沒有「這個月」這類運算子。受支援的相對時間條件是滾動視窗;日曆區間則取決於呼叫端的時區與週界定義。要表達日曆區間,請在呼叫端算出上下界,再用 between 明確傳入。

修正IaC

手寫的觸發器行不會再永遠 drift#

手寫的觸發器行以前每次 apply 都會被重新規劃成 update,因為儲存端會補上兩個作者從未輸入的欄位。現在差異比對與執行端共用同一個正規化邏輯來補齊,所以第二次 plan 就是 noop,這才是應有的行為。

10 月 5–6 日補齊說明已合併 CT 來源,不更新離線 catalog/OpenAPI 或 Playground。來源界線包含已合併座標 PR1 #1988。#1994 僅涵蓋 CT 附件 gate,不涵蓋非 CT surface 或 rollout。主管確認 #1987 已合併並於 5.19.0 發行;實際出勤套件仍須獨立驗收。

10 月 6 日發行目標 — 座標 PR1(5.20.0)

主管將已合併 PR1 加上 #1987、#2015 設為 backend 5.20.0 發行目標。下列 source pin 保留該來源自己的 VERSION;此文件不驗證 production rollout。

新增 — #1988,合併 master c15a12f9b01aa3e88e7b0cf65fe4735a48e03718(該來源 VERSION 為 5.19.0):座標儲存、幾何、formula/Command/rule 運算子及出勤 how-to均對應 PR1。Capture ingress 預設拒絕;不確定 check 拒絕、approval/immutable gate 保持保護、trigger 記錄 reason 並跳過,含幾何的 Command null condition 拒絕。原點/filter trust、disclosure 拒絕、LINE parsing 與推論/device-spoofing residual 明列。空間查詢篩選不屬於 5.20.0;它們隨 5.21.0 提供(見下方 10 月 8 日一節)。 不推定座標 rollout 或瀏覽器驗收。

10 月 6 日來源增補 — proposal 失敗與 read 證據

修正 — #2015,已合併來源 3085346728403c45e7a4d832223fdcd7d895d43d(VERSION 5.19.0):凍結提案區分真正過期證據與非預期 HTTP 500 ct.operation_failed,保留 typed retry/constraint/key-conflict 回應與不帶私密值的根本原因診斷,並從整表 logical source-column read 證據排除呼叫者的隱藏欄位。已提交收據的不確定仍須查詢 receipt。這只是來源教學文件,不代表 rollout、行為驗收或離線 OpenAPI/catalog 更新。

10 月 6 日來源增補 — history 與 column ACL

修正/變更 — #1987,已合併來源 b1e5a13d0e209b41cca460fbe6ba0dc7c7b301a8:歷程套用 canonical column denial、現行 schema ceiling 與兩邊 snapshot 的 computed provenance。受欄位 allowlist/Agent ceiling 限制的 principal 不會取得已移除欄位;未受限讀者仍可讀取已移除的 stored field。Column ACL輸入可使用目前 display name,但儲存會正規化成 internal key;未知或衝突別名會被拒絕。

**需調整:**直接呈現伺服器過濾後的 data/diff,不從歷史 schema 補回被省略的欄位;保留改名穩定的 internal identity,不把 display-keyed ACL payload 當作實際儲存 key。此 source-only 增補不更新離線 OpenAPI/Playground、不證明 rollout,也不放行出勤 execute grants。Owner 已回報整合後座標一般讀者/管理員隱私控制通過;實際出勤套件仍有獨立 gate。

10 月 8 日發行 — backend 5.21.0

Backend 5.21.0(172a7f80bdf4dbdffdc018eb08bfa42c62bb485c,tag v5.21.0)接續 5.20.1(4a6921b7e2b1b09f9c9cd5615210e3cc234fe364)。上方的發行紀錄列出每項呼叫端可見的變更與它動到的端點,CT kernel 補齊則把每項變更對應到 backend PR。這裡的內容不驗證任何部署;離線 OpenAPI 快照、端點目錄與 Playground 仍固定在各自較早的版本,所以新的 request 欄位只在概念頁說明。上面 10 月 6 日把空間查詢篩選寫成「預計在後續版本提供」的說明,指的是 backend 5.20.0;這些篩選隨 5.21.0 提供。

升級時請先檢查

  1. 其他公司的 id 回 404。 不要再依 403 Insufficient permissions. (Different Company) 分支:其他公司的資料表、房間、link 目標、診斷或 trigger 目標 id,現在的回應與不存在的 id 相同;個別使用者的 permissions 授權路由則維持單一 403。在先檢查房間的聊天室路由上,路徑中不存在或已刪除的聊天室 id,現在會在查詢資料表之前就回 404 Chatroom Not found,即使資料表屬於你自己的公司也一樣。見其他公司的 id 回應與不存在相同。
  2. 大型 IaC 欄位異動回 202。 文件中的 column 行若要回填超過 10,000 筆 live 紀錄的資料表,就走有 ticket 的路徑套用:請輪詢 ticket 並讀取 body.result。見重量級欄位變更。
  3. 超過 500 筆 live 紀錄的 REST 欄位新增改在背景執行。 回填結束前,紀錄寫入會回 423,請追蹤 migration_id 或 table migration status。見背景新增欄位。
  4. 匯出的形狀變了。 試算表可能當成公式執行的 CSV 標頭或文字 cell,現在會以撇號加零寬空白開頭;超過位元組上限的匯出回 413,產生時間超過時間預算的回 422。見匯出檔案。
  5. 把帶 dependency_unavailable 的 503 視為可重試。 新增欄位、刪除欄位或清除單一資料表時遇到 Redis 中斷,不再回帶驅動程式文字的 500(清除時一次性的 ticket 已經用掉,請重新預覽)。見併發與重試。
  6. 用正確的憑證輪詢 ticket。 匯入 extraction 的 ticket 現在只對上傳檔案的使用者開放(publish commit 與 IaC apply 的 ticket 原本就是如此),IaC apply 與匯入 extraction 的 id 是 version-8 UUID,每一次拒絕都是同一個 404 Task not found。見誰可以輪詢哪種 ticket。

同一版的新功能:座標查詢、Command 的 on_conflict upsert step 與本地時間與日期運算子、作者選擇公開的結果,以及工作日行事曆的唯讀 API key 存取。

10 月 9 日發行 — backend 5.22.0

Backend 5.22.0(36270f5a2b83fdd71b6641ad94cee707f8028292,即 tag v5.22.0 的程式碼樹)接續 5.21.0(172a7f80bdf4dbdffdc018eb08bfa42c62bb485c)。上方的發行紀錄列出每項呼叫端可見的變更與它動到的端點,CT kernel 補齊則把每項變更對應到 backend PR。這裡的內容不驗證任何部署;離線 OpenAPI 快照、端點目錄與 Playground 仍固定在各自較早的版本,所以新的 request 欄位只在概念頁說明。

升級時請先檢查

  1. 處理 IaC apply 的 202。 plan 建立、更新、刪除或搬移超過 300 個資源(record 行與 no-op 不計入)的 apply,會改走 ticket 套用:request 回 202 與 ticket,而不是在 request 內套用後回 200,所以請輪詢 ticket 並讀取 body.result。只把 200 當成功的 client 遇到這類文件就會出問題;對這類文件明確指定 worker=legacy,除非 legacy 是部署預設值,否則會被 400 ct.iac_legacy_backfill_unavailable 拒絕。見變更量大的 Plan。
  2. 含座標欄位的資料表,請重新 plan、重新準備或重新暫存一次。 升級前凍結的 plan_hash、Command 提案或暫存世代會失效一次。座標索引狀態也不再是資料表 settings 的一部分:任何讀取都不回傳,帶它的建立請求會被拒絕,5.21.0 匯出的 IaC 文件仍可匯入(在 5.21.0 上傳、文件帶有這個 key 的 package input 必須重新上傳)。見已審查 Hash 與 409與座標索引。
  3. 公開讀取的 query 最多接受 3 個 sort 鍵。 第 4 個鍵回 400,過去 4 到 10 個鍵會以 500 結束。見讀取。
  4. definer 權限 Command 的 dry run,對 execute-only 呼叫者會像執行一樣被遮蔽: 預期拿到帶 redacted: true 的 command_failed,而不是引擎的詳細內容(會原樣通過的錯誤碼與公開的 assertion 訊息,和執行時一樣仍看得到)。見 Status-only 結果。

同一版的新功能:附件複製,讓 Command 憑 grant,代原本無法自行複製的呼叫者(例如外部 client)跨資料表複製指定附件。修正:鎖衝突不再讓背景新增欄位永久失敗;被隱藏的欄位在另外三處的回應與不存在的欄位相同;對不含 child_collection step 的 Command 呼叫 /execute 時,若核准規則的來源資料表落在 tag 之外,改回 422 command_tables_outside_tag 而不是 500;apply 準時回傳的 IaC ticket 以 finished 結束;單一 API worker 收到大量已驗證請求時,請求也不再互相卡住。

11 月 4 日更新 — 效能專案的契約

Backend master 3856f7710f473602367ae2580b16047a4d2ffbc1(該 commit 的 VERSION 為 5.32.1)收錄 Custom Tables 的效能專案:#2816、#2806、#2807、#2805 與 #2808,以及 #2644 的 ?name= 查詢。疊在它之上合併的 #2820(8da4670c7a8a0b6ce281f963b8fb515a58eba49f)讓 Agent 工具箱、MCP 與 workspace SDK 採用同一套 cursor 與筆數契約;更正 #2824(已合併為 7eff5613e))則讓 POST …/records/count 和搜尋一樣,不對整合 API key 開放。每一項呼叫端會察覺到的變更與它的端點,都列在上方的發行紀錄。本次只涵蓋這些變更;5.22.0 到這些 commit 之間的其他變更,這裡尚未記錄。這裡沒有任何內容驗證過部署狀況。新的路由與有變動的 schema,放在一份小型的 OpenAPI overlay 中,疊在仍固定於 7c41cbc5 的快照之上(見 README 的「Post-pin overlay」),所以參考手冊看得到它們。

請先檢查這幾項

  1. 把列表畫面改成 cursor 分頁。 送 page_mode=cursor,沿著 next_cursor 一直讀到 has_more 為 false,並且不要再讀 total 與 table.record_count(cursor 模式下兩者都是 null)。沒有帶 page_mode 的請求行為不變,但 total 已列為棄用,等前端遷移完成後,會連同 offset 與 skip 一起移除。見cursor 分頁與筆數。
  2. 要顯示數字的地方才算筆數。 POST …/records/count 接受搜尋的 body,回傳精確的 count;每次搜尋呼叫一次,不要每一頁都呼叫。和搜尋一樣,它接受使用者的 JWT:third_party、specific_chatroom 或 acquisition scope 的整合 API key 會被拒絕,回 405(#2816 起初接受這類 key,#2824 把它移除)。見大量資料的效能做法。
  3. 選單改用指令摘要。 GET …/command-summaries 列出相同的指令但不含定義;GET …/commands?name= 取得單一支。見列出與查找指令。
  4. 讀取資料列有時間上限。 分頁與筆數的查詢(包含 offset 分頁)預設 15 秒後回 400 ct.query_timeout。請縮小篩選範圍,或改用 cursor 分頁。
  5. Agent、MCP 與 SDK 的分頁方式相同。 助理的 custom_tables_query_records 與 custom_tables_find_records 回傳 has_more 與 next_cursor,沒有總數;MCP 的 records_query 接受 page_mode 與 cursor;精確筆數則是另外的請求(operation: "count"、records_count 工具),只在有人問「有幾筆」時才發出。見 Agent 工具箱。

同一版的新功能:純量陣列指令 input 與 $in、bulk-insert ticket 的耗時,以及對送出 Accept-Encoding: gzip 的呼叫端壓縮大型 JSON 回應。

日期採 Asia/Taipei。Staging 狀態表示可在測試環境串接,不表示尚未進 production;已驗證的 production rollout 會在該筆紀錄註明 release 與驗證範圍。

Last updated on