paseo-plugin-zcode-provider 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +29 -0
- package/README.md +112 -2
- package/client/account.tsx +827 -0
- package/client/diagnostics.tsx +379 -0
- package/client/runtime.tsx +296 -0
- package/client/settings.tsx +84 -0
- package/client/value.tsx +20 -0
- package/icon.svg +1 -0
- package/index.client.tsx +22 -0
- package/index.server.ts +39 -0
- package/package.json +77 -4
- package/paseo-plugin.json +6 -0
- package/server/account.ts +781 -0
- package/server/attachments.ts +150 -0
- package/server/build-info.ts +2 -0
- package/server/conversation.ts +319 -0
- package/server/diagnostics.ts +252 -0
- package/server/discovery/discover.ts +225 -0
- package/server/discovery/manifest.ts +36 -0
- package/server/discovery/types.ts +30 -0
- package/server/errors.ts +36 -0
- package/server/host/bridge.ts +490 -0
- package/server/host/schemas.ts +119 -0
- package/server/logger.ts +19 -0
- package/server/mapping.ts +270 -0
- package/server/models.ts +68 -0
- package/server/persistence.ts +146 -0
- package/server/presentation.ts +470 -0
- package/server/provider-status.ts +81 -0
- package/server/provider.ts +838 -0
- package/server/runtime/managed.ts +501 -0
- package/server/runtime/pins.ts +72 -0
- package/server/runtime-setup.ts +77 -0
- package/server/session-types.ts +49 -0
- package/server/session.ts +866 -0
- package/server/status.ts +89 -0
- package/server/timeline.ts +18 -0
- package/server/usage.ts +362 -0
- package/server/vendor/zcode/LICENSE +202 -0
- package/server/vendor/zcode/NOTICE.md +70 -0
- package/server/vendor/zcode/THIRD-PARTY-NOTICES.md +37 -0
- package/server/vendor/zcode/packages/model-option-map/src/compiler.ts +92 -0
- package/server/vendor/zcode/packages/model-option-map/src/evaluator.ts +214 -0
- package/server/vendor/zcode/packages/model-option-map/src/index.ts +5 -0
- package/server/vendor/zcode/packages/model-option-map/src/merge-patch.ts +97 -0
- package/server/vendor/zcode/packages/model-option-map/src/option-maps.ts +42 -0
- package/server/vendor/zcode/packages/model-option-map/src/parser.ts +240 -0
- package/server/vendor/zcode/packages/model-option-map/src/tokenizer.ts +141 -0
- package/server/vendor/zcode/packages/model-option-map/src/types.ts +36 -0
- package/server/vendor/zcode/packages/rpc/src/buffer.ts +88 -0
- package/server/vendor/zcode/packages/rpc/src/channelClient.ts +275 -0
- package/server/vendor/zcode/packages/rpc/src/channelServer.ts +283 -0
- package/server/vendor/zcode/packages/rpc/src/channels.shared.ts +66 -0
- package/server/vendor/zcode/packages/rpc/src/channels.ts +4 -0
- package/server/vendor/zcode/packages/rpc/src/delayedChannel.ts +17 -0
- package/server/vendor/zcode/packages/rpc/src/foundation.ts +260 -0
- package/server/vendor/zcode/packages/rpc/src/protocol.ts +397 -0
- package/server/vendor/zcode/packages/rpc/src/proxy-channel.ts +213 -0
- package/server/vendor/zcode/packages/rpc/src/serialization.ts +291 -0
- package/server/vendor/zcode/packages/shared/src/account-provider-state.ts +13 -0
- package/server/vendor/zcode/packages/shared/src/background-bash-output.ts +24 -0
- package/server/vendor/zcode/packages/shared/src/background-task-controls.ts +404 -0
- package/server/vendor/zcode/packages/shared/src/bash-output-display.ts +11 -0
- package/server/vendor/zcode/packages/shared/src/bots.ts +558 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/backend.ts +80 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/command-metadata.ts +170 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/commands.ts +588 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/result.ts +130 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/snapshot.ts +77 -0
- package/server/vendor/zcode/packages/shared/src/command-types.ts +96 -0
- package/server/vendor/zcode/packages/shared/src/config-schema.ts +8 -0
- package/server/vendor/zcode/packages/shared/src/cuaAccessibilitySettings.ts +41 -0
- package/server/vendor/zcode/packages/shared/src/database-startup.ts +178 -0
- package/server/vendor/zcode/packages/shared/src/env.ts +63 -0
- package/server/vendor/zcode/packages/shared/src/execution-output-preview.ts +14 -0
- package/server/vendor/zcode/packages/shared/src/localTtft.ts +192 -0
- package/server/vendor/zcode/packages/shared/src/model-config.ts +123 -0
- package/server/vendor/zcode/packages/shared/src/model-execution.ts +23 -0
- package/server/vendor/zcode/packages/shared/src/model-provider-family.ts +171 -0
- package/server/vendor/zcode/packages/shared/src/model-provider-types.ts +81 -0
- package/server/vendor/zcode/packages/shared/src/model-selection.ts +60 -0
- package/server/vendor/zcode/packages/shared/src/oauth.ts +203 -0
- package/server/vendor/zcode/packages/shared/src/official-mcp-auth.ts +272 -0
- package/server/vendor/zcode/packages/shared/src/official-mcp-tool-error.ts +68 -0
- package/server/vendor/zcode/packages/shared/src/process-diagnostic.ts +42 -0
- package/server/vendor/zcode/packages/shared/src/protocol.ts +369 -0
- package/server/vendor/zcode/packages/shared/src/provider-family-connection-selection.ts +32 -0
- package/server/vendor/zcode/packages/shared/src/remoteAssetInstallMode.ts +11 -0
- package/server/vendor/zcode/packages/shared/src/remoteResourcePackages.ts +91 -0
- package/server/vendor/zcode/packages/shared/src/runtimeEnv.ts +315 -0
- package/server/vendor/zcode/packages/shared/src/settings-source.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/usage-quota.ts +74 -0
- package/server/vendor/zcode/packages/shared/src/usage-stats.ts +462 -0
- package/server/vendor/zcode/packages/shared/src/validationAppSettings.ts +562 -0
- package/server/vendor/zcode/packages/shared/src/workspacePurpose.ts +2 -0
- package/server/vendor/zcode/packages/shared/src/wslUserValidation.ts +33 -0
- package/server/vendor/zcode/packages/shared/src/zcode-agent-policy.ts +25 -0
- package/server/vendor/zcode/packages/shared/src/zcode-media-policy.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol/index.ts +3720 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-legacy-types.ts +529 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/apply.ts +218 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/attachment-ref.ts +14 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/command.ts +482 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/core.ts +100 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/create-workflow-display.ts +156 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/cuaPermission.ts +41 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/delta.ts +147 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/input-intent.ts +69 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/rows.ts +432 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/session-config.ts +43 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/sessions-index-workflow-activity.ts +195 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/sessions-index.ts +85 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/shared-context-import.ts +38 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/shared-context-ref.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/snapshot.ts +509 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/submission.ts +5 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/toolDisplay.ts +264 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/transport.ts +1152 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-assembler.ts +574 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-binary.ts +65 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-codec.ts +240 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-fault.ts +39 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire.ts +159 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-artifacts.ts +291 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-observation-display.ts +281 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-row-meta.ts +163 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-run-settings-command.ts +62 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-runs-delta.ts +517 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-runs.ts +512 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workspace-config.ts +81 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workspace-hook-review.ts +210 -0
- package/server/vendor/zcode/packages/shared/src/zcode-task-mode-schema.ts +6 -0
- package/server/vendor/zcode/packages/shared/src/zcode-task-types-core.ts +1187 -0
- package/server/vendor/zcode/packages/shared/src/zcodeEndpoint.ts +296 -0
- package/server/vendor/zcode/provenance.json +405 -0
- package/shared/account.ts +201 -0
- package/shared/diagnostics.ts +52 -0
- package/shared/runtime-setup.ts +73 -0
- package/vendor/acp-registry/LICENSE +176 -0
- package/vendor/paseo/LICENSE +211 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// workflowRuns 的键级增量:diff(生产侧)、apply(消费侧)与规范键序
|
|
3
|
+
// ============================================================
|
|
4
|
+
// 为什么存在:`workflowRuns` 是一个**高频**状态键,而 `state.updated` 的语义是键级整体替换。
|
|
5
|
+
// 一条引擎事件只改一个节点,却要把整张表重发一遍——每事件 O(N) 字节、一条 run 全程 O(N²)。
|
|
6
|
+
// 节点/子代理被压在 256 上、宽 fan-out 一撞界就静默丢实例,根子都在这里。本模块把「这一步
|
|
7
|
+
// 改了什么」算出来,让线上的字节数与**改动量**成正比,而不是与状态大小成正比。
|
|
8
|
+
//
|
|
9
|
+
// 三条结构性事实,读下面的代码前先知道,否则几处会像兜底:
|
|
10
|
+
// 1. reducer 只在表满时**淘汰**终态条目,绝不重排(workflow-runs-eviction.ts)。所以 diff 的
|
|
11
|
+
// 快路径仍然是按下标对齐比较,只在对不齐时才退到键化路径算出「哪些走了」;真遇到重排
|
|
12
|
+
// 它认不出来,只能整键重发(见 RESYNC)。
|
|
13
|
+
// 2. reducer 只给**变化的那条 run** 造新对象,其余 run 与未改动的条目保持引用不变。所以
|
|
14
|
+
// diff 的第一道闸是引用比较,深比较只发生在真正动过的那条 run 上。
|
|
15
|
+
// 3. 正确性绝不依赖引用:引用相等只是快路径,不等时一律退回结构比较。
|
|
16
|
+
//
|
|
17
|
+
// 契约:对任意一对 reducer 产出的 (prior, next),
|
|
18
|
+
// `JSON.stringify(applyAll(带 prior 的快照, diff(prior, next)).workflowRuns) === JSON.stringify(next)`
|
|
19
|
+
// ——**逐字节**,不只是深相等。键序因此是本模块的一等公民,见 {@link canonicalWorkflowRun}。
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
ConversationDelta,
|
|
23
|
+
WorkflowRunEntryRef,
|
|
24
|
+
WorkflowRunHeaderKey,
|
|
25
|
+
WorkflowRunRemovedDelta,
|
|
26
|
+
WorkflowRunUpdatedDelta,
|
|
27
|
+
} from "./delta.js";
|
|
28
|
+
import {
|
|
29
|
+
WORKFLOW_RUNS_LIMITS,
|
|
30
|
+
workflowRunSchema,
|
|
31
|
+
type WorkflowRunActor,
|
|
32
|
+
type WorkflowRunNode,
|
|
33
|
+
type WorkflowRunState,
|
|
34
|
+
type WorkflowRunsState,
|
|
35
|
+
} from "./workflow-runs.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* run 对象的**规范键序** = `workflowRunSchema` 的声明序。
|
|
39
|
+
*
|
|
40
|
+
* 逐字节一致里唯一不靠「值相等」保证的一环就是键序:reducer 用 `{...run, 新键: v}` 推进状态,
|
|
41
|
+
* 新出现的可选键(reports / phases / concurrency…)因此按**到达顺序**缀在对象尾部,而 apply
|
|
42
|
+
* 重建 run 时没有那段历史。两边各自按这张表重排一次,序就对齐了——这也是为什么 reducer 的
|
|
43
|
+
* 出口同样要走一遍本函数(那是一处,不是两处)。
|
|
44
|
+
*
|
|
45
|
+
* 键序从 schema 派生,避免独立维护字段表导致生产者和消费者使用不同的 run 结构。
|
|
46
|
+
*/
|
|
47
|
+
const WORKFLOW_RUN_KEYS = Object.keys(workflowRunSchema.shape) as (keyof WorkflowRunState)[];
|
|
48
|
+
const WORKFLOW_RUN_KEY_SET: ReadonlySet<string> = new Set<string>(WORKFLOW_RUN_KEYS);
|
|
49
|
+
|
|
50
|
+
/** header = run 减去两张按 (siteId, ordinal) 增量同步的表。顺序仍是 schema 声明序。 */
|
|
51
|
+
export const WORKFLOW_RUN_HEADER_KEYS: readonly WorkflowRunHeaderKey[] = WORKFLOW_RUN_KEYS.filter(
|
|
52
|
+
(key): key is WorkflowRunHeaderKey => key !== "actors" && key !== "nodes",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* header 里的**必填**键,同样从 schema 派生(`safeParse(undefined)` 通过 = 可缺省)。
|
|
57
|
+
* 它只用在一处:判一条 `workflowRun.updated` 的 header 够不够格让一条未知 run **出生**。
|
|
58
|
+
*/
|
|
59
|
+
const runShape = workflowRunSchema.shape as unknown as Record<
|
|
60
|
+
string,
|
|
61
|
+
{ safeParse: (value: unknown) => { success: boolean } }
|
|
62
|
+
>;
|
|
63
|
+
const WORKFLOW_RUN_REQUIRED_HEADER_KEYS: readonly WorkflowRunHeaderKey[] =
|
|
64
|
+
WORKFLOW_RUN_HEADER_KEYS.filter((key) => !runShape[key]!.safeParse(undefined).success);
|
|
65
|
+
|
|
66
|
+
/** 两张实例表的去重键。`\0` 分隔的理由与 reducer 的派生 actor 状态逐字相同:("a",12) 与 ("a1",2) 不能撞车。 */
|
|
67
|
+
export function workflowRunEntryKey(entry: { siteId: string; ordinal: number }): string {
|
|
68
|
+
return `${entry.siteId}\0${entry.ordinal}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 按规范键序重建一个 run 对象(浅重建:嵌套值原样搬运引用,它们本来就来自同一份事实)。
|
|
73
|
+
*
|
|
74
|
+
* `undefined` 值的键按缺席处理,与 `JSON.stringify` 的语义对齐——协议线上只有「键在场」与
|
|
75
|
+
* 「键缺席」两态,没有第三态。schema 之外的键按原序缀在尾部:本模块没有资格替调用方丢数据。
|
|
76
|
+
*/
|
|
77
|
+
export function canonicalWorkflowRun(run: WorkflowRunState): WorkflowRunState {
|
|
78
|
+
const source = run as unknown as Record<string, unknown>;
|
|
79
|
+
const canonical: Record<string, unknown> = {};
|
|
80
|
+
for (const key of WORKFLOW_RUN_KEYS) {
|
|
81
|
+
const value = source[key];
|
|
82
|
+
if (value !== undefined) canonical[key] = value;
|
|
83
|
+
}
|
|
84
|
+
for (const key of Object.keys(source)) {
|
|
85
|
+
if (WORKFLOW_RUN_KEY_SET.has(key)) continue;
|
|
86
|
+
const value = source[key];
|
|
87
|
+
if (value !== undefined) canonical[key] = value;
|
|
88
|
+
}
|
|
89
|
+
return canonical as unknown as WorkflowRunState;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* JSON 值的结构相等。reducer 的幂等判据(原来是整条 run 的 `JSON.stringify` 比对,每事件
|
|
94
|
+
* O(N) 字节)与 diff 的「这个键/条目变了吗」共用这一份,两处判据不会再各说各话。
|
|
95
|
+
*
|
|
96
|
+
* 与 `JSON.stringify` 的差别只有两处,两处都是**更**准确:键序不参与判定;`undefined` 值的键
|
|
97
|
+
* 与缺席等同(stringify 也会丢掉它们)。
|
|
98
|
+
*/
|
|
99
|
+
export function jsonValueEqual(a: unknown, b: unknown): boolean {
|
|
100
|
+
if (a === b) return true;
|
|
101
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
102
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
103
|
+
return a.every((item, index) => jsonValueEqual(item, b[index]));
|
|
104
|
+
}
|
|
105
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
106
|
+
const left = a as Record<string, unknown>;
|
|
107
|
+
const right = b as Record<string, unknown>;
|
|
108
|
+
let present = 0;
|
|
109
|
+
for (const key of Object.keys(left)) {
|
|
110
|
+
const value = left[key];
|
|
111
|
+
if (value === undefined) continue;
|
|
112
|
+
present += 1;
|
|
113
|
+
if (!jsonValueEqual(value, right[key])) return false;
|
|
114
|
+
}
|
|
115
|
+
let expected = 0;
|
|
116
|
+
for (const key of Object.keys(right)) if (right[key] !== undefined) expected += 1;
|
|
117
|
+
return present === expected;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** reducer 的幂等判据:同一条事件重放后内容无变化即不产 delta、不抬 revision。 */
|
|
121
|
+
export function workflowRunUnchanged(previous: WorkflowRunState, next: WorkflowRunState): boolean {
|
|
122
|
+
return jsonValueEqual(previous, next);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 一条 header 够不够格让**未知 run 出生**:必填键一个不缺。
|
|
127
|
+
*
|
|
128
|
+
* 只有 diff 的「诞生」分支会造出完整 header——已有 run 的增量里 `runId` 永远不变、因此永远不在
|
|
129
|
+
* patch 里,于是若干条增量合并(coalesce 规则 6)也**拼不出**一条完整 header。这条不变量是
|
|
130
|
+
* 合并规则得以成立的支点:合并不会把两条对未知 run 的 no-op 变成一次凭空出生。
|
|
131
|
+
*/
|
|
132
|
+
export function isCompleteWorkflowRunHeader(header: unknown): boolean {
|
|
133
|
+
if (typeof header !== "object" || header === null) return false;
|
|
134
|
+
const record = header as Record<string, unknown>;
|
|
135
|
+
return WORKFLOW_RUN_REQUIRED_HEADER_KEYS.every((key) => record[key] !== undefined);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 两个 workflowRuns 状态之间的键级增量。
|
|
140
|
+
*
|
|
141
|
+
* 退路只有一条:认不出的结构变化(条目被删或被重排,见文件头第 1 条)或「没有任何 op 却
|
|
142
|
+
* revision 动了」时,整键重发一条 `state.updated`。它在协议上完全合法(键级整体替换),
|
|
143
|
+
* 在 coalesce 里是一道屏障,代价只是退回改造前的字节数。
|
|
144
|
+
*/
|
|
145
|
+
export function diffWorkflowRunsState(
|
|
146
|
+
prior: WorkflowRunsState | undefined,
|
|
147
|
+
next: WorkflowRunsState,
|
|
148
|
+
): ConversationDelta[] {
|
|
149
|
+
const priorRuns = prior?.runs ?? [];
|
|
150
|
+
const nextIds = new Set(next.runs.map((run) => run.runId));
|
|
151
|
+
const ops: ConversationDelta[] = [];
|
|
152
|
+
// 淘汰先发、按 prior 序:客户端先把走了的 run 摘掉,剩下的增量都作用在活着的表上。
|
|
153
|
+
for (const run of priorRuns) {
|
|
154
|
+
if (nextIds.has(run.runId)) continue;
|
|
155
|
+
ops.push({ op: "workflowRun.removed", runId: run.runId, revision: next.revision });
|
|
156
|
+
}
|
|
157
|
+
const priorById = new Map(priorRuns.map((run) => [run.runId, run]));
|
|
158
|
+
for (const run of next.runs) {
|
|
159
|
+
const before = priorById.get(run.runId);
|
|
160
|
+
if (before === run) continue;
|
|
161
|
+
const op = diffWorkflowRun(before, run, next.revision);
|
|
162
|
+
if (op === null) return [{ op: "state.updated", patch: { workflowRuns: next } }];
|
|
163
|
+
if (op !== undefined) ops.push(op);
|
|
164
|
+
}
|
|
165
|
+
// 一条 op 都没有却 revision 变了:容器版本没有自己的载体,只能整键重发。
|
|
166
|
+
// reducer 不产生这种输入(无变化时它返回 null),所以这条在生产上不会走到。
|
|
167
|
+
if (ops.length === 0 && (prior?.revision ?? -1) !== next.revision) {
|
|
168
|
+
return [{ op: "state.updated", patch: { workflowRuns: next } }];
|
|
169
|
+
}
|
|
170
|
+
return ops;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 单条 run 的增量:`undefined` = 无变化,`null` = 认不出的结构变化(整键重发)。 */
|
|
174
|
+
function diffWorkflowRun(
|
|
175
|
+
before: WorkflowRunState | undefined,
|
|
176
|
+
run: WorkflowRunState,
|
|
177
|
+
revision: number,
|
|
178
|
+
): WorkflowRunUpdatedDelta | undefined | null {
|
|
179
|
+
const source = run as unknown as Record<string, unknown>;
|
|
180
|
+
// key 是 header 键的联合,逐键写回时 TS 会把值收窄成 never;这里按记录装配、出口处再定型。
|
|
181
|
+
const patch: Record<string, unknown> = {};
|
|
182
|
+
const cleared: WorkflowRunHeaderKey[] = [];
|
|
183
|
+
if (before === undefined) {
|
|
184
|
+
// 诞生:整条 header(含 runId,客户端据它判断这条 op 够不够格建表)+ 全部条目。
|
|
185
|
+
for (const key of WORKFLOW_RUN_HEADER_KEYS) {
|
|
186
|
+
const value = source[key];
|
|
187
|
+
if (value !== undefined) patch[key] = value;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
op: "workflowRun.updated",
|
|
191
|
+
runId: run.runId,
|
|
192
|
+
revision,
|
|
193
|
+
run: patch as WorkflowRunUpdatedDelta["run"],
|
|
194
|
+
...(run.actors.length > 0 ? { actors: [...run.actors] } : {}),
|
|
195
|
+
...(run.nodes.length > 0 ? { nodes: [...run.nodes] } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const previous = before as unknown as Record<string, unknown>;
|
|
199
|
+
for (const key of WORKFLOW_RUN_HEADER_KEYS) {
|
|
200
|
+
const left = previous[key];
|
|
201
|
+
const right = source[key];
|
|
202
|
+
if (left === right) continue;
|
|
203
|
+
if (right === undefined) {
|
|
204
|
+
cleared.push(key);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (left !== undefined && jsonValueEqual(left, right)) continue;
|
|
208
|
+
patch[key] = right;
|
|
209
|
+
}
|
|
210
|
+
const actors = diffWorkflowRunEntries(before.actors, run.actors);
|
|
211
|
+
const nodes = diffWorkflowRunEntries(before.nodes, run.nodes);
|
|
212
|
+
if (actors === null || nodes === null) return null;
|
|
213
|
+
const changedHeader = Object.keys(patch).length > 0 || cleared.length > 0;
|
|
214
|
+
if (!changedHeader && actors === undefined && nodes === undefined) return undefined;
|
|
215
|
+
// 键序 = 施加序(header → 删除 → upsert):同一个键被删又被加时它落在表尾,与顺序施加
|
|
216
|
+
// 两条 op 一致。
|
|
217
|
+
return {
|
|
218
|
+
op: "workflowRun.updated",
|
|
219
|
+
runId: run.runId,
|
|
220
|
+
revision,
|
|
221
|
+
...(Object.keys(patch).length > 0 ? { run: patch as WorkflowRunUpdatedDelta["run"] } : {}),
|
|
222
|
+
...(cleared.length > 0 ? { cleared } : {}),
|
|
223
|
+
...(actors?.removed.length ? { removedActors: actors.removed } : {}),
|
|
224
|
+
...(nodes?.removed.length ? { removedNodes: nodes.removed } : {}),
|
|
225
|
+
...(actors?.changed.length ? { actors: actors.changed } : {}),
|
|
226
|
+
...(nodes?.changed.length ? { nodes: nodes.changed } : {}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 一张实例表这一步的变化:走掉的条目(只带身份)+ 变了的条目(整条,按 next 的顺序)。 */
|
|
231
|
+
interface EntryDiff<T> {
|
|
232
|
+
removed: WorkflowRunEntryRef[];
|
|
233
|
+
changed: T[];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 两张实例表之间的变化。`undefined` = 无变化,`null` = 认不出的结构变化(整键重发)。
|
|
238
|
+
*
|
|
239
|
+
* 快路径按**下标对齐**比较:reducer 的常态是追加与原地更新,这条路径上未改动的条目只做一次
|
|
240
|
+
* 指针比较。第一次对不齐(下标处身份不同,或 prior 比 next 长)说明有条目走了——那是腾位
|
|
241
|
+
* (workflow-runs-eviction.ts),于是退到键化路径把「哪些走了」算出来。
|
|
242
|
+
*
|
|
243
|
+
* 键化路径仍然要求**幸存者按下标对齐**:删除之外的顺序变化这个模型表达不了(协议里没有
|
|
244
|
+
* 条目移动语法),只能整键重发。
|
|
245
|
+
*/
|
|
246
|
+
function diffWorkflowRunEntries<T extends { siteId: string; ordinal: number }>(
|
|
247
|
+
before: readonly T[],
|
|
248
|
+
after: readonly T[],
|
|
249
|
+
): EntryDiff<T> | undefined | null {
|
|
250
|
+
if (before === after) return undefined;
|
|
251
|
+
if (before.length <= after.length) {
|
|
252
|
+
const changed: T[] = [];
|
|
253
|
+
let aligned = true;
|
|
254
|
+
for (let index = 0; index < after.length && aligned; index += 1) {
|
|
255
|
+
const entry = after[index]!;
|
|
256
|
+
const previous = before[index];
|
|
257
|
+
if (previous === undefined) {
|
|
258
|
+
changed.push(entry);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (previous === entry) continue;
|
|
262
|
+
if (previous.siteId !== entry.siteId || previous.ordinal !== entry.ordinal) aligned = false;
|
|
263
|
+
else if (!jsonValueEqual(previous, entry)) changed.push(entry);
|
|
264
|
+
}
|
|
265
|
+
if (aligned) return changed.length > 0 ? { removed: [], changed } : undefined;
|
|
266
|
+
}
|
|
267
|
+
return diffWorkflowRunEntriesByKey(before, after);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function diffWorkflowRunEntriesByKey<T extends { siteId: string; ordinal: number }>(
|
|
271
|
+
before: readonly T[],
|
|
272
|
+
after: readonly T[],
|
|
273
|
+
): EntryDiff<T> | undefined | null {
|
|
274
|
+
const survives = new Set(after.map(workflowRunEntryKey));
|
|
275
|
+
const removed: WorkflowRunEntryRef[] = [];
|
|
276
|
+
const survivors: T[] = [];
|
|
277
|
+
for (const entry of before) {
|
|
278
|
+
if (survives.has(workflowRunEntryKey(entry))) survivors.push(entry);
|
|
279
|
+
else removed.push({ siteId: entry.siteId, ordinal: entry.ordinal });
|
|
280
|
+
}
|
|
281
|
+
if (survivors.length > after.length) return null;
|
|
282
|
+
const changed: T[] = [];
|
|
283
|
+
for (let index = 0; index < after.length; index += 1) {
|
|
284
|
+
const entry = after[index]!;
|
|
285
|
+
const previous = survivors[index];
|
|
286
|
+
if (previous === undefined) {
|
|
287
|
+
changed.push(entry);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// 幸存者必须仍按原序坐在 next 的前缀上,否则就是重排——退回整键重发。
|
|
291
|
+
if (previous.siteId !== entry.siteId || previous.ordinal !== entry.ordinal) return null;
|
|
292
|
+
if (previous === entry || jsonValueEqual(previous, entry)) continue;
|
|
293
|
+
changed.push(entry);
|
|
294
|
+
}
|
|
295
|
+
if (removed.length === 0 && changed.length === 0) return undefined;
|
|
296
|
+
return { removed, changed };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* `workflowRun.updated` 的应用。
|
|
301
|
+
*
|
|
302
|
+
* 容器 revision 取 **max**:coalesce 允许把靠后的 op 合并到靠前的位置上(规则 6),于是输出序列
|
|
303
|
+
* 的最后一条不一定携带最高 revision。revision 按契约单调,max 让「合并前后终态逐字节一致」这条
|
|
304
|
+
* 定律不依赖 op 的位置。
|
|
305
|
+
*
|
|
306
|
+
* 身份:未改动的 run 与未改动的条目保持引用不变,变化的那条 run 与容器是新对象——GUI 正是按
|
|
307
|
+
* 这两级引用做 memo。
|
|
308
|
+
*/
|
|
309
|
+
export function applyWorkflowRunUpdated(
|
|
310
|
+
state: WorkflowRunsState | undefined,
|
|
311
|
+
delta: WorkflowRunUpdatedDelta,
|
|
312
|
+
): WorkflowRunsState {
|
|
313
|
+
const runs = state?.runs ?? [];
|
|
314
|
+
const revision = Math.max(state?.revision ?? 0, delta.revision);
|
|
315
|
+
const index = runs.findIndex((run) => run.runId === delta.runId);
|
|
316
|
+
if (index < 0) {
|
|
317
|
+
// 未知 run + 完整 header = 诞生;未知 run + 残缺 header = 这条 op 说的不是客户端手上的事实,
|
|
318
|
+
// 按 no-op 处理(与 row.upserted 命中未加载行同一条裁决),只让容器 revision 跟上。
|
|
319
|
+
if (!isCompleteWorkflowRunHeader(delta.run)) {
|
|
320
|
+
return state !== undefined && state.revision === revision ? state : { revision, runs };
|
|
321
|
+
}
|
|
322
|
+
const born = canonicalWorkflowRun({
|
|
323
|
+
...(delta.run as Partial<WorkflowRunState>),
|
|
324
|
+
actors: delta.actors === undefined ? [] : [...delta.actors],
|
|
325
|
+
nodes: delta.nodes === undefined ? [] : [...delta.nodes],
|
|
326
|
+
} as WorkflowRunState);
|
|
327
|
+
return { revision, runs: [...runs, born] };
|
|
328
|
+
}
|
|
329
|
+
const existing = runs[index]!;
|
|
330
|
+
const merged: Record<string, unknown> = { ...existing, ...delta.run };
|
|
331
|
+
// 「零条 ⇒ 键缺席」是好几个键的协议约定,所以增量必须说得出「这个键没了」。
|
|
332
|
+
for (const key of delta.cleared ?? []) delete merged[key];
|
|
333
|
+
// 施加序:header → 删除 → upsert。同一个键在一条 op 里被删又被加时它落在表尾,与顺序施加
|
|
334
|
+
// 两条 op 的结果一致;反过来(先加后删)会把一条刚回来的条目又摘掉。
|
|
335
|
+
const actors = removeWorkflowRunEntries(existing.actors, delta.removedActors);
|
|
336
|
+
const nodes = removeWorkflowRunEntries(existing.nodes, delta.removedNodes);
|
|
337
|
+
if (actors !== existing.actors) merged.actors = actors;
|
|
338
|
+
if (nodes !== existing.nodes) merged.nodes = nodes;
|
|
339
|
+
if (delta.actors !== undefined && delta.actors.length > 0) {
|
|
340
|
+
merged.actors = upsertWorkflowRunEntries(actors, delta.actors);
|
|
341
|
+
}
|
|
342
|
+
if (delta.nodes !== undefined && delta.nodes.length > 0) {
|
|
343
|
+
merged.nodes = upsertWorkflowRunEntries(nodes, delta.nodes);
|
|
344
|
+
}
|
|
345
|
+
const next = [...runs];
|
|
346
|
+
next[index] = canonicalWorkflowRun(merged as unknown as WorkflowRunState);
|
|
347
|
+
return { revision, runs: next };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** `workflowRun.removed` 的应用:把这条 run 摘掉。未知 runId 只让 revision 跟上。 */
|
|
351
|
+
export function applyWorkflowRunRemoved(
|
|
352
|
+
state: WorkflowRunsState | undefined,
|
|
353
|
+
delta: WorkflowRunRemovedDelta,
|
|
354
|
+
): WorkflowRunsState {
|
|
355
|
+
const runs = state?.runs ?? [];
|
|
356
|
+
const revision = Math.max(state?.revision ?? 0, delta.revision);
|
|
357
|
+
const remaining = runs.filter((run) => run.runId !== delta.runId);
|
|
358
|
+
if (remaining.length === runs.length) {
|
|
359
|
+
return state !== undefined && state.revision === revision ? state : { revision, runs };
|
|
360
|
+
}
|
|
361
|
+
return { revision, runs: remaining };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 按 (siteId, ordinal) 摘掉条目;一条都没命中时返回**同一个数组**(未提到的表要保住引用)。
|
|
366
|
+
* 未命中的键是定义明确的 no-op:合并把两条 op 的删除取了并集,其中一些在这个客户端手上
|
|
367
|
+
* 根本没到过。
|
|
368
|
+
*/
|
|
369
|
+
function removeWorkflowRunEntries<T extends { siteId: string; ordinal: number }>(
|
|
370
|
+
current: readonly T[],
|
|
371
|
+
removed: readonly { siteId: string; ordinal: number }[] | undefined,
|
|
372
|
+
): T[] {
|
|
373
|
+
if (removed === undefined || removed.length === 0) return current as T[];
|
|
374
|
+
const dropped = new Set(removed.map(workflowRunEntryKey));
|
|
375
|
+
const next = current.filter((entry) => !dropped.has(workflowRunEntryKey(entry)));
|
|
376
|
+
return next.length === current.length ? (current as T[]) : next;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 按 (siteId, ordinal) upsert:已有键原地替换、新键追加到尾部。
|
|
381
|
+
*
|
|
382
|
+
* 客户端**绝不**施加任何上界(maxNodes / maxActors / maxRuns):只有生产者有资格淘汰,而且
|
|
383
|
+
* 淘汰必须说出来(`workflowRun.removed` / `removedActors` / `removedNodes`)。客户端自行裁剪
|
|
384
|
+
* 只会让两侧悄悄分叉。
|
|
385
|
+
*/
|
|
386
|
+
function upsertWorkflowRunEntries<T extends { siteId: string; ordinal: number }>(
|
|
387
|
+
current: readonly T[],
|
|
388
|
+
incoming: readonly T[],
|
|
389
|
+
): T[] {
|
|
390
|
+
const next = [...current];
|
|
391
|
+
const indexByKey = new Map<string, number>();
|
|
392
|
+
next.forEach((entry, index) => indexByKey.set(workflowRunEntryKey(entry), index));
|
|
393
|
+
for (const entry of incoming) {
|
|
394
|
+
const key = workflowRunEntryKey(entry);
|
|
395
|
+
const index = indexByKey.get(key);
|
|
396
|
+
if (index === undefined) {
|
|
397
|
+
indexByKey.set(key, next.length);
|
|
398
|
+
next.push(entry);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
next[index] = entry;
|
|
402
|
+
}
|
|
403
|
+
return next;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 一条 `workflowRun.updated` 的四张条目表都在线上界之内吗。
|
|
408
|
+
*
|
|
409
|
+
* 真正会超界的是**两张删除表**。合并出来的 upsert 表自己就有界:按合并规则,先者 upsert 的键
|
|
410
|
+
* 凡被后者删掉的都已去掉,所以留下的每一个键在这条 op 施加完之后都还在表里,而表本身 ≤ 界。
|
|
411
|
+
* 删除表没有这条护栏——合并分不出「窗口里刚出生又被淘汰的键」和「客户端早就拿着的键」,
|
|
412
|
+
* 每次淘汰都让出一个位子,一个够宽的 flush 窗口里被删掉的不同键因此可以多于 1024 条。
|
|
413
|
+
* 淘汰之前这不可能:表只增不减,一世之内被碰过的不同键最多就是表界那么多。
|
|
414
|
+
*
|
|
415
|
+
* 超界载荷会让整个 patch 解析失败、整帧被丢;publisher 的两道限制(500 op / 1 MiB)在这个
|
|
416
|
+
* 量级上都不会响。四张表都查是因为这道闸很便宜,而且不该依赖上面那条论证一直成立。
|
|
417
|
+
*
|
|
418
|
+
* 合并是**优化**,所以拒绝合并永远是安全的:两条 op 各自都在界内,逐条投递的终态一个字节都不差。
|
|
419
|
+
*/
|
|
420
|
+
export function workflowRunUpdateWithinWireBounds(
|
|
421
|
+
delta: WorkflowRunUpdatedDelta,
|
|
422
|
+
limits: WorkflowRunWireBounds = WORKFLOW_RUNS_LIMITS,
|
|
423
|
+
): boolean {
|
|
424
|
+
return (
|
|
425
|
+
(delta.actors?.length ?? 0) <= limits.maxActors &&
|
|
426
|
+
(delta.removedActors?.length ?? 0) <= limits.maxActors &&
|
|
427
|
+
(delta.nodes?.length ?? 0) <= limits.maxNodes &&
|
|
428
|
+
(delta.removedNodes?.length ?? 0) <= limits.maxNodes
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 条目表的容量上限,默认使用 {@link WORKFLOW_RUNS_LIMITS}。
|
|
434
|
+
* 增量应用和合并必须使用一致的上限。
|
|
435
|
+
*/
|
|
436
|
+
export interface WorkflowRunWireBounds {
|
|
437
|
+
readonly maxActors: number;
|
|
438
|
+
readonly maxNodes: number;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** 合并两条同 run 的 `workflowRun.updated`(coalesce 规则 6 的载荷部分,规则本身在 coalesce.ts)。 */
|
|
442
|
+
export function mergeWorkflowRunUpdates(
|
|
443
|
+
earlier: WorkflowRunUpdatedDelta,
|
|
444
|
+
later: WorkflowRunUpdatedDelta,
|
|
445
|
+
): WorkflowRunUpdatedDelta {
|
|
446
|
+
const run: Record<string, unknown> = { ...earlier.run, ...later.run };
|
|
447
|
+
// header 键与 `cleared` 互为反面,合并时必须彼此对消:后设值的键不再是"被清掉的",
|
|
448
|
+
// 后清掉的键也不再有值。留一半会让 apply 先写后删(或先删后写),结果取决于键序。
|
|
449
|
+
const laterCleared = new Set<string>(later.cleared ?? []);
|
|
450
|
+
for (const key of laterCleared) delete run[key];
|
|
451
|
+
const cleared: WorkflowRunHeaderKey[] = [];
|
|
452
|
+
for (const key of earlier.cleared ?? []) {
|
|
453
|
+
if (run[key] === undefined && !cleared.includes(key)) cleared.push(key);
|
|
454
|
+
}
|
|
455
|
+
for (const key of later.cleared ?? []) {
|
|
456
|
+
if (!cleared.includes(key)) cleared.push(key);
|
|
457
|
+
}
|
|
458
|
+
const removedActors = mergeRemovedRefs(earlier.removedActors, later.removedActors);
|
|
459
|
+
const removedNodes = mergeRemovedRefs(earlier.removedNodes, later.removedNodes);
|
|
460
|
+
const actors = mergeEntryLists<WorkflowRunActor>(
|
|
461
|
+
earlier.actors,
|
|
462
|
+
later.actors,
|
|
463
|
+
later.removedActors,
|
|
464
|
+
);
|
|
465
|
+
const nodes = mergeEntryLists<WorkflowRunNode>(earlier.nodes, later.nodes, later.removedNodes);
|
|
466
|
+
return {
|
|
467
|
+
op: "workflowRun.updated",
|
|
468
|
+
runId: later.runId,
|
|
469
|
+
// 取最高水位:合并后的 op 坐在靠前的位置上,靠 apply 的 max 让容器版本仍然收在最高 revision。
|
|
470
|
+
revision: Math.max(earlier.revision, later.revision),
|
|
471
|
+
...(Object.keys(run).length > 0 ? { run: run as WorkflowRunUpdatedDelta["run"] } : {}),
|
|
472
|
+
...(cleared.length > 0 ? { cleared } : {}),
|
|
473
|
+
...(removedActors === undefined ? {} : { removedActors }),
|
|
474
|
+
...(removedNodes === undefined ? {} : { removedNodes }),
|
|
475
|
+
...(actors === undefined ? {} : { actors }),
|
|
476
|
+
...(nodes === undefined ? {} : { nodes }),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** 两侧的删除取并集,按首次出现去重(删除是按键过滤,顺序不影响结果,只影响字节)。 */
|
|
481
|
+
function mergeRemovedRefs(
|
|
482
|
+
earlier: readonly WorkflowRunEntryRef[] | undefined,
|
|
483
|
+
later: readonly WorkflowRunEntryRef[] | undefined,
|
|
484
|
+
): WorkflowRunEntryRef[] | undefined {
|
|
485
|
+
if (earlier === undefined && later === undefined) return undefined;
|
|
486
|
+
const merged: WorkflowRunEntryRef[] = [];
|
|
487
|
+
const seen = new Set<string>();
|
|
488
|
+
for (const ref of [...(earlier ?? []), ...(later ?? [])]) {
|
|
489
|
+
const key = workflowRunEntryKey(ref);
|
|
490
|
+
if (seen.has(key)) continue;
|
|
491
|
+
seen.add(key);
|
|
492
|
+
merged.push(ref);
|
|
493
|
+
}
|
|
494
|
+
return merged.length > 0 ? merged : undefined;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 条目表按键后来者覆盖、按首次出现保序;两侧都没有条目时返回 undefined(键不建)。
|
|
499
|
+
*
|
|
500
|
+
* **先者 upsert 的键凡是被后者删掉的一律去掉**:顺序施加时它先进表、再被后者摘走,所以合并后
|
|
501
|
+
* 的 op 里根本不该有它。若后者又把同一个键加了回来,它只在后者的表里出现一次,于是落在表尾
|
|
502
|
+
* ——顺序施加得到的正是这个位置。
|
|
503
|
+
*/
|
|
504
|
+
function mergeEntryLists<T extends { siteId: string; ordinal: number }>(
|
|
505
|
+
earlier: readonly T[] | undefined,
|
|
506
|
+
later: readonly T[] | undefined,
|
|
507
|
+
laterRemoved: readonly WorkflowRunEntryRef[] | undefined,
|
|
508
|
+
): T[] | undefined {
|
|
509
|
+
if (earlier === undefined && later === undefined) return undefined;
|
|
510
|
+
const dropped = new Set((laterRemoved ?? []).map(workflowRunEntryKey));
|
|
511
|
+
const kept =
|
|
512
|
+
dropped.size === 0
|
|
513
|
+
? (earlier ?? [])
|
|
514
|
+
: (earlier ?? []).filter((entry) => !dropped.has(workflowRunEntryKey(entry)));
|
|
515
|
+
const merged = upsertWorkflowRunEntries(kept, later ?? []);
|
|
516
|
+
return merged.length > 0 ? merged : undefined;
|
|
517
|
+
}
|