dsh-kingdom 0.8.0 → 2.0.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.
Files changed (162) hide show
  1. package/ASSETS_NOTICE.md +22 -0
  2. package/LICENSE +661 -29
  3. package/NOTICE +9 -0
  4. package/README.md +93 -94
  5. package/THIRD_PARTY_NOTICES.md +92 -0
  6. package/cordis.patch.yml +6 -6
  7. package/docs/V2.0-RELEASE-NOTES.md +30 -0
  8. package/docs/V2.0-USER-GUIDE.md +74 -0
  9. package/lib/adapter/dsh-backend.js +327 -24
  10. package/lib/adapter/dsh-backend.js.map +1 -1
  11. package/lib/adapter/dsh-cost.js +68 -0
  12. package/lib/adapter/dsh-cost.js.map +1 -0
  13. package/lib/adapter/dsh-session-events.js +86 -0
  14. package/lib/adapter/dsh-session-events.js.map +1 -0
  15. package/lib/adapter/dsh-usage.js +159 -0
  16. package/lib/adapter/dsh-usage.js.map +1 -0
  17. package/lib/capability/admin.js +72 -0
  18. package/lib/capability/admin.js.map +1 -0
  19. package/lib/capability/dsh-enforcement.js +143 -34
  20. package/lib/capability/dsh-enforcement.js.map +1 -1
  21. package/lib/capability/resolver.js +21 -8
  22. package/lib/capability/resolver.js.map +1 -1
  23. package/lib/capability/service.js +169 -26
  24. package/lib/capability/service.js.map +1 -1
  25. package/lib/capability/tool-disclosure.js +144 -0
  26. package/lib/capability/tool-disclosure.js.map +1 -0
  27. package/lib/core/binding.js +52 -34
  28. package/lib/core/binding.js.map +1 -1
  29. package/lib/core/budget.js +304 -0
  30. package/lib/core/budget.js.map +1 -0
  31. package/lib/core/collaboration.js +422 -0
  32. package/lib/core/collaboration.js.map +1 -0
  33. package/lib/core/cost.js +212 -0
  34. package/lib/core/cost.js.map +1 -0
  35. package/lib/core/db.js +197 -4
  36. package/lib/core/db.js.map +1 -1
  37. package/lib/core/execution.js +2 -2
  38. package/lib/core/execution.js.map +1 -1
  39. package/lib/core/governed-claim.js +46 -0
  40. package/lib/core/governed-claim.js.map +1 -0
  41. package/lib/core/governed.js +891 -98
  42. package/lib/core/governed.js.map +1 -1
  43. package/lib/core/kingdom.js +78 -70
  44. package/lib/core/kingdom.js.map +1 -1
  45. package/lib/core/owner-binding-intent.js +732 -0
  46. package/lib/core/owner-binding-intent.js.map +1 -0
  47. package/lib/core/owner-control.js +86 -0
  48. package/lib/core/owner-control.js.map +1 -0
  49. package/lib/core/owner-window.js +822 -0
  50. package/lib/core/owner-window.js.map +1 -0
  51. package/lib/core/task-service.js +296 -133
  52. package/lib/core/task-service.js.map +1 -1
  53. package/lib/core/territory.js +44 -14
  54. package/lib/core/territory.js.map +1 -1
  55. package/lib/core/usage.js +39 -0
  56. package/lib/core/usage.js.map +1 -0
  57. package/lib/core/workspace-admission.js +225 -0
  58. package/lib/core/workspace-admission.js.map +1 -0
  59. package/lib/dispatch/evidence.js +5 -2
  60. package/lib/dispatch/evidence.js.map +1 -1
  61. package/lib/dispatch/reconcile.js +3 -9
  62. package/lib/dispatch/reconcile.js.map +1 -1
  63. package/lib/dispatch/service.js +464 -64
  64. package/lib/dispatch/service.js.map +1 -1
  65. package/lib/gui/assets/characters/chancellor-idle.svg +67 -0
  66. package/lib/gui/assets/characters/chancellor-sleeping.svg +67 -0
  67. package/lib/gui/assets/characters/chancellor-thinking.svg +67 -0
  68. package/lib/gui/assets/characters/chancellor-working.svg +67 -0
  69. package/lib/gui/assets/characters/knight-redraw-r1-idle.svg +67 -0
  70. package/lib/gui/assets/characters/knight-redraw-r1-sleeping.svg +67 -0
  71. package/lib/gui/assets/characters/knight-redraw-r1-thinking.svg +67 -0
  72. package/lib/gui/assets/characters/knight-redraw-r1-working.svg +67 -0
  73. package/lib/gui/assets/characters/supervisor-idle.svg +67 -0
  74. package/lib/gui/assets/characters/supervisor-sleeping.svg +67 -0
  75. package/lib/gui/assets/characters/supervisor-thinking.svg +67 -0
  76. package/lib/gui/assets/characters/supervisor-working.svg +67 -0
  77. package/lib/gui/console-app.js +2330 -0
  78. package/lib/gui/console-app.js.map +1 -0
  79. package/lib/gui/contract.js +5 -5
  80. package/lib/gui/contract.js.map +1 -1
  81. package/lib/gui/control-contract.js +232 -0
  82. package/lib/gui/control-contract.js.map +1 -0
  83. package/lib/gui/local-control.js +423 -0
  84. package/lib/gui/local-control.js.map +1 -0
  85. package/lib/gui/owner-control.js +310 -0
  86. package/lib/gui/owner-control.js.map +1 -0
  87. package/lib/gui/owner-ui.js +191 -0
  88. package/lib/gui/owner-ui.js.map +1 -0
  89. package/lib/gui/server.js +445 -35
  90. package/lib/gui/server.js.map +1 -1
  91. package/lib/gui/snapshot.js +1346 -67
  92. package/lib/gui/snapshot.js.map +1 -1
  93. package/lib/gui/visual-assets.js +56 -0
  94. package/lib/gui/visual-assets.js.map +1 -0
  95. package/lib/gui/visual-system.js +272 -0
  96. package/lib/gui/visual-system.js.map +1 -0
  97. package/lib/gui/workbench-ui.js +358 -0
  98. package/lib/gui/workbench-ui.js.map +1 -0
  99. package/lib/gui/workbench.js +163 -0
  100. package/lib/gui/workbench.js.map +1 -0
  101. package/lib/index.js +1384 -361
  102. package/lib/index.js.map +1 -1
  103. package/lib/runner-context-broker.js +1134 -0
  104. package/lib/runner-context-broker.js.map +1 -0
  105. package/lib/runtime-cost-observer.js +145 -0
  106. package/lib/runtime-cost-observer.js.map +1 -0
  107. package/lib/types/adapter/contract.d.ts +61 -1
  108. package/lib/types/adapter/dsh-backend.d.ts +32 -13
  109. package/lib/types/adapter/dsh-cost.d.ts +13 -0
  110. package/lib/types/adapter/dsh-session-events.d.ts +16 -0
  111. package/lib/types/adapter/dsh-usage.d.ts +36 -0
  112. package/lib/types/capability/admin.d.ts +13 -0
  113. package/lib/types/capability/dsh-enforcement.d.ts +11 -7
  114. package/lib/types/capability/service.d.ts +10 -3
  115. package/lib/types/capability/tool-disclosure.d.ts +36 -0
  116. package/lib/types/core/binding.d.ts +19 -10
  117. package/lib/types/core/budget.d.ts +81 -0
  118. package/lib/types/core/collaboration.d.ts +110 -0
  119. package/lib/types/core/cost.d.ts +99 -0
  120. package/lib/types/core/db.d.ts +44 -4
  121. package/lib/types/core/execution.d.ts +1 -1
  122. package/lib/types/core/governed-claim.d.ts +7 -0
  123. package/lib/types/core/governed.d.ts +182 -1
  124. package/lib/types/core/kingdom.d.ts +3 -0
  125. package/lib/types/core/owner-binding-intent.d.ts +176 -0
  126. package/lib/types/core/owner-control.d.ts +53 -0
  127. package/lib/types/core/owner-window.d.ts +226 -0
  128. package/lib/types/core/task-service.d.ts +60 -13
  129. package/lib/types/core/territory.d.ts +9 -2
  130. package/lib/types/core/usage.d.ts +26 -0
  131. package/lib/types/core/workspace-admission.d.ts +43 -0
  132. package/lib/types/dispatch/evidence.d.ts +2 -5
  133. package/lib/types/dispatch/service.d.ts +62 -4
  134. package/lib/types/gui/console-app.d.ts +137 -0
  135. package/lib/types/gui/contract.d.ts +396 -16
  136. package/lib/types/gui/control-contract.d.ts +111 -0
  137. package/lib/types/gui/local-control.d.ts +120 -0
  138. package/lib/types/gui/owner-control.d.ts +45 -0
  139. package/lib/types/gui/owner-ui.d.ts +2 -0
  140. package/lib/types/gui/server.d.ts +17 -3
  141. package/lib/types/gui/snapshot.d.ts +33 -11
  142. package/lib/types/gui/visual-assets.d.ts +27 -0
  143. package/lib/types/gui/visual-system.d.ts +2 -0
  144. package/lib/types/gui/workbench-ui.d.ts +5 -0
  145. package/lib/types/gui/workbench.d.ts +33 -0
  146. package/lib/types/index.d.ts +157 -6
  147. package/lib/types/runner-context-broker.d.ts +119 -0
  148. package/lib/types/runtime-cost-observer.d.ts +22 -0
  149. package/lib/types/worker/collaboration.d.ts +35 -0
  150. package/lib/types/worker/dsh-subagent.d.ts +4 -4
  151. package/lib/types/worker/executor-factory.d.ts +2 -1
  152. package/lib/types/worker/executor.d.ts +8 -3
  153. package/lib/types/worker/governed-executor.d.ts +8 -1
  154. package/lib/worker/collaboration.js +172 -0
  155. package/lib/worker/collaboration.js.map +1 -0
  156. package/lib/worker/dsh-subagent.js +4 -4
  157. package/lib/worker/executor-factory.js.map +1 -1
  158. package/lib/worker/executor.js +7 -2
  159. package/lib/worker/executor.js.map +1 -1
  160. package/lib/worker/governed-executor.js +295 -84
  161. package/lib/worker/governed-executor.js.map +1 -1
  162. package/package.json +23 -8
@@ -1,5 +1,20 @@
1
+ /**
2
+ * dsh-kingdom — DB 行 → GUI 视图的投影(Phase 3)。
3
+ *
4
+ * 全部是**纯函数**:给定 (库状态, now) 就唯一确定输出。
5
+ * 因此 GUI 轮询即可拿到正确的表演状态,服务端不需要任何定时器或推送状态机。
6
+ *
7
+ * 再次强调边界:本文件只产出 `{ role, state, activity }` 这类语义,
8
+ * 绝不产出贴图、clip、场景文件名——那些是 GUI 的 visual-map 的事。
9
+ */
10
+ import { readDispatchUsage } from '../core/usage.js';
11
+ import { readAdditionalRoleCost, readLatestPromptCosts } from '../core/cost.js';
12
+ import { readBudgetView } from '../core/budget.js';
13
+ import { listCollaborationPlans, readCollaborationReadiness, readPlanBudgetView } from '../core/collaboration.js';
14
+ import { readWorkspaceReservations } from '../core/workspace-admission.js';
15
+ import { buildPersonalWorkbench } from './workbench.js';
1
16
  import { GUI_SCHEMA_VERSION, TRANSIENT_WINDOW_MS } from './contract.js';
2
- import { asExecutionState, isLiveExecutionState } from '../core/execution.js';
17
+ import { asExecutionState, isLiveExecutionState, isTerminalExecutionState } from '../core/execution.js';
3
18
  import { asTaskStatus } from '../core/task.js';
4
19
  import { parseExecutionProfile } from '../core/binding.js';
5
20
  // ── 行 → 视图 ───────────────────────────────────────────────────
@@ -17,6 +32,379 @@ function parseJson(text) {
17
32
  function stringList(value) {
18
33
  return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
19
34
  }
35
+ const REDACTED_JSON_VALUE = '[REDACTED]';
36
+ const TRUNCATED_JSON_DEPTH = '[TRUNCATED_DEPTH]';
37
+ const TRUNCATED_JSON_NODES = '[TRUNCATED_NODES]';
38
+ const TRUNCATED_JSON_ARRAY = '[TRUNCATED_ARRAY]';
39
+ const TRUNCATED_JSON_OBJECT_KEY = '__projectionTruncated__';
40
+ const MAX_PUBLIC_JSON_DEPTH = 6;
41
+ const MAX_PUBLIC_JSON_ENTRIES = 32;
42
+ const MAX_PUBLIC_JSON_NODES = 512;
43
+ const MAX_PUBLIC_JSON_STRING = 512;
44
+ const PUBLIC_JSON_TRUNCATION_SUFFIX = '…[TRUNCATED]';
45
+ const SENSITIVE_JSON_KEY_FRAGMENTS = [
46
+ 'session',
47
+ 'principal',
48
+ 'private',
49
+ 'config',
50
+ 'token',
51
+ 'cookie',
52
+ 'credential',
53
+ 'secret',
54
+ 'password',
55
+ 'authorization',
56
+ 'apikey',
57
+ 'accesskey',
58
+ 'clientsecret',
59
+ 'bearer',
60
+ 'connectionstring',
61
+ 'csrf',
62
+ 'authority',
63
+ 'ownercontrol',
64
+ ];
65
+ function sensitivePublicJsonKey(key) {
66
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/gu, '');
67
+ return SENSITIVE_JSON_KEY_FRAGMENTS.some(fragment => normalized.includes(fragment));
68
+ }
69
+ function sanitizePublicJsonString(value, sensitivity = 'public-content') {
70
+ if (sensitivity === 'private-config')
71
+ return REDACTED_JSON_VALUE;
72
+ const redacted = value
73
+ .replace(/(?:authorization|bearer|session(?:[_ .-]?id)?|principal(?:[_ .-]?id)?|private(?:[_ .-]?config)?|token|cookie|credential|secret|password|api[_ .-]?key|access[_ .-]?key|client[_ .-]?secret|connection[_ .-]?string|csrf)\b(?:\s*["']?\s*(?:=|:)\s*["']?\s*|\s+)[^\s,;&}]+/giu, REDACTED_JSON_VALUE)
74
+ .replace(/[A-Za-z]:[\\/][^\s<>"']*/gu, '[redacted-path]')
75
+ .replace(/\\\\[^\s<>"']*/gu, '[redacted-path]')
76
+ .replace(/(^|[\s("'=])\/[^\s<>"']*/gu, '$1[redacted-path]')
77
+ .replace(/\bprivate[-_][^\s<>"']+/giu, REDACTED_JSON_VALUE);
78
+ if (redacted.length <= MAX_PUBLIC_JSON_STRING)
79
+ return redacted;
80
+ return `${redacted.slice(0, MAX_PUBLIC_JSON_STRING - PUBLIC_JSON_TRUNCATION_SUFFIX.length)}${PUBLIC_JSON_TRUNCATION_SUFFIX}`;
81
+ }
82
+ function sanitizePublicJsonValue(value, depth, budget) {
83
+ if (budget.remaining <= 0)
84
+ return TRUNCATED_JSON_NODES;
85
+ budget.remaining -= 1;
86
+ if (value === null || typeof value === 'boolean' || typeof value === 'number')
87
+ return value;
88
+ if (typeof value === 'string')
89
+ return sanitizePublicJsonString(value);
90
+ if (Array.isArray(value)) {
91
+ if (depth >= MAX_PUBLIC_JSON_DEPTH)
92
+ return TRUNCATED_JSON_DEPTH;
93
+ const result = value
94
+ .slice(0, MAX_PUBLIC_JSON_ENTRIES)
95
+ .map(item => sanitizePublicJsonValue(item, depth + 1, budget));
96
+ if (value.length > MAX_PUBLIC_JSON_ENTRIES)
97
+ result.push(TRUNCATED_JSON_ARRAY);
98
+ return result;
99
+ }
100
+ if (typeof value !== 'object')
101
+ return null;
102
+ if (depth >= MAX_PUBLIC_JSON_DEPTH)
103
+ return TRUNCATED_JSON_DEPTH;
104
+ const entries = Object.entries(value);
105
+ const result = Object.create(null);
106
+ for (const [key, item] of entries.slice(0, MAX_PUBLIC_JSON_ENTRIES)) {
107
+ result[key] = sensitivePublicJsonKey(key)
108
+ ? REDACTED_JSON_VALUE
109
+ : sanitizePublicJsonValue(item, depth + 1, budget);
110
+ }
111
+ if (entries.length > MAX_PUBLIC_JSON_ENTRIES) {
112
+ result[TRUNCATED_JSON_OBJECT_KEY] = entries.length - MAX_PUBLIC_JSON_ENTRIES;
113
+ }
114
+ return result;
115
+ }
116
+ /** Event payload 与 Binding session metadata 共用的递归、确定性、有界公共 JSON 转换。 */
117
+ function sanitizePublicJsonObject(value) {
118
+ const sanitized = sanitizePublicJsonValue(value, 0, { remaining: MAX_PUBLIC_JSON_NODES });
119
+ return typeof sanitized === 'object' && sanitized !== null && !Array.isArray(sanitized)
120
+ ? sanitized
121
+ : {};
122
+ }
123
+ function sanitizePublicStringList(value) {
124
+ const sanitized = sanitizePublicJsonValue(stringList(value), 0, { remaining: MAX_PUBLIC_JSON_NODES });
125
+ return Array.isArray(sanitized)
126
+ ? sanitized.filter((item) => typeof item === 'string')
127
+ : [];
128
+ }
129
+ // ── v0.9 S1 只读 Projection ──────────────────────────────────────
130
+ const MAX_PROJECTION_SOURCE_REFS = 8;
131
+ const MAX_PROJECTION_TIMELINE_ITEMS = 200;
132
+ const MAX_PROJECTION_TEXT = 140;
133
+ const MAX_PROJECTION_ORGANIZATION_ITEMS = 64;
134
+ const MAX_PROJECTION_EXECUTION_SUMMARIES = 100;
135
+ const OWNER_ONLY_ACTIONS = [
136
+ 'init',
137
+ 'reset',
138
+ 'ceiling',
139
+ 'territory.create',
140
+ 'territory.delete',
141
+ 'territory.supervisor',
142
+ // v0.9 aliases remain additive for existing JSON consumers.
143
+ 'binding.bind',
144
+ 'binding.unbind',
145
+ 'binding.session',
146
+ // Canonical direct Slash command names used by v1.0.
147
+ 'role.bind',
148
+ 'role.unbind',
149
+ 'role.session',
150
+ 'execution-profile',
151
+ ];
152
+ const PUBLIC_ENTITY_REF_TYPES = new Set([
153
+ 'kingdom',
154
+ 'task',
155
+ 'execution',
156
+ 'territory',
157
+ 'binding',
158
+ 'affinity',
159
+ 'lease',
160
+ 'decision',
161
+ 'dispatch',
162
+ ]);
163
+ const PUBLIC_TABLE_SOURCE_ENTITY_TYPES = new Set([
164
+ 'kingdoms',
165
+ 'tasks',
166
+ 'territories',
167
+ 'role_bindings',
168
+ 'task_assignments',
169
+ 'worker_results',
170
+ 'executions',
171
+ 'worker_session_affinities',
172
+ 'execution_leases',
173
+ 'capability_decisions',
174
+ 'dispatch_records',
175
+ ]);
176
+ function publicReferenceId(value) {
177
+ if (!value)
178
+ return null;
179
+ const trimmed = value.trim();
180
+ return /^[A-Za-z0-9_.:@-]{1,96}$/u.test(trimmed) ? trimmed : null;
181
+ }
182
+ function boundedText(value) {
183
+ if (!value)
184
+ return '';
185
+ return sanitizePublicJsonString(value)
186
+ .replace(/[\r\n\t]+/gu, ' ')
187
+ .trim()
188
+ .slice(0, MAX_PROJECTION_TEXT);
189
+ }
190
+ function publicEntityRef(type, id) {
191
+ if (!PUBLIC_ENTITY_REF_TYPES.has(type))
192
+ return null;
193
+ const safeId = publicReferenceId(id);
194
+ return safeId ? { type, id: safeId } : null;
195
+ }
196
+ function tableSource(entityType, entityId) {
197
+ return { sourceType: 'table-row', entityType, entityId: publicReferenceId(entityId) };
198
+ }
199
+ function eventSource(seq) {
200
+ return { sourceType: 'event', entityType: 'events', entityId: null, eventSeq: seq };
201
+ }
202
+ function ruleSource(ruleCode) {
203
+ return { sourceType: 'derived-rule', entityType: 'projection-rule', entityId: null, ruleCode };
204
+ }
205
+ function publicSourceType(value) {
206
+ return value === 'table-row'
207
+ || value === 'event'
208
+ || value === 'runtime-evidence'
209
+ || value === 'derived-rule'
210
+ ? value
211
+ : 'derived-rule';
212
+ }
213
+ function publicSourceEntityType(sourceType, value) {
214
+ const safeValue = boundedText(value);
215
+ if (sourceType === 'table-row' && PUBLIC_TABLE_SOURCE_ENTITY_TYPES.has(safeValue))
216
+ return safeValue;
217
+ if (sourceType === 'event' && safeValue === 'events')
218
+ return safeValue;
219
+ if (sourceType === 'runtime-evidence' && safeValue === 'runtime-observation')
220
+ return safeValue;
221
+ if (sourceType === 'derived-rule' && safeValue === 'projection-rule')
222
+ return safeValue;
223
+ return 'redacted-source';
224
+ }
225
+ function publicRuleCode(value) {
226
+ if (!value)
227
+ return undefined;
228
+ const trimmed = value.trim();
229
+ return /^[A-Z][A-Z0-9_]{0,95}$/u.test(trimmed) ? trimmed : 'REDACTED_RULE';
230
+ }
231
+ /** 对外暴露前统一限制数量与标识格式;不接受 session/raw payload/path 作为引用。 */
232
+ export function boundedSourceRefs(refs) {
233
+ const result = [];
234
+ const seen = new Set();
235
+ for (const ref of refs) {
236
+ const sourceType = publicSourceType(ref.sourceType);
237
+ const entityType = publicSourceEntityType(sourceType, ref.entityType);
238
+ const ruleCode = sourceType === 'derived-rule' ? publicRuleCode(ref.ruleCode) : undefined;
239
+ const normalized = {
240
+ sourceType,
241
+ entityType,
242
+ entityId: sourceType === 'table-row' && entityType !== 'redacted-source'
243
+ ? publicReferenceId(ref.entityId)
244
+ : null,
245
+ ...(sourceType === 'event' && Number.isInteger(ref.eventSeq) && ref.eventSeq >= 0
246
+ ? { eventSeq: ref.eventSeq }
247
+ : {}),
248
+ ...(ruleCode ? { ruleCode } : {}),
249
+ };
250
+ const key = JSON.stringify(normalized);
251
+ if (seen.has(key))
252
+ continue;
253
+ seen.add(key);
254
+ result.push(normalized);
255
+ if (result.length >= MAX_PROJECTION_SOURCE_REFS)
256
+ break;
257
+ }
258
+ return result;
259
+ }
260
+ function safeDate(value) {
261
+ if (!value)
262
+ return null;
263
+ const timestamp = Date.parse(value);
264
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
265
+ }
266
+ function projectionEnvelope(input) {
267
+ return {
268
+ revision: input.revision,
269
+ refreshedAt: input.refreshedAt,
270
+ entityRef: input.entityRef,
271
+ authoritativeState: input.authoritativeState,
272
+ sourceRefs: boundedSourceRefs(input.sourceRefs),
273
+ allowedActions: input.allowedActions ?? null,
274
+ attentionReason: input.attentionReason ?? null,
275
+ data: input.data,
276
+ };
277
+ }
278
+ function attentionReason(code, refs) {
279
+ return { code, sourceRefs: boundedSourceRefs(refs) };
280
+ }
281
+ function executionTerminality(state) {
282
+ if (state === 'COMPLETED' || state === 'FAILED' || state === 'ABORTED')
283
+ return 'TERMINAL';
284
+ if (state === 'STARTING' || state === 'RUNNING' || state === 'PAUSED')
285
+ return 'NON_TERMINAL';
286
+ return 'UNKNOWN';
287
+ }
288
+ /** 未终结的 governed attempt(含 RECOVERING)在完成对账前绝不能产生新 start。 */
289
+ function isGovernedStartBlocked(execution) {
290
+ return execution?.execution_contract === 'GOVERNED_PERSISTENT'
291
+ && !isTerminalExecutionState(asExecutionState(execution.state));
292
+ }
293
+ function stateValue(value, sourceKind, refs) {
294
+ return { sourceKind, value: boundedText(value) || 'UNKNOWN', sourceRefs: boundedSourceRefs(refs) };
295
+ }
296
+ function timelineId(prefix, id) {
297
+ return `${prefix}:${publicReferenceId(id) ?? 'unknown'}`;
298
+ }
299
+ function actionBinding(store, task) {
300
+ const territory = store.getTerritoryById(task.territory_id);
301
+ return territory?.supervisor_binding_id ? store.getBindingById(territory.supervisor_binding_id) : null;
302
+ }
303
+ function commandForAction(action) {
304
+ if (action.startsWith('review:'))
305
+ return 'review';
306
+ if (action.startsWith('execution:'))
307
+ return action.replace(':', '.');
308
+ return action;
309
+ }
310
+ function firstMissingActionContext(store, task, action, security) {
311
+ const refs = [tableSource('tasks', task.task_id)];
312
+ const principalSessionId = security.principalSessionId;
313
+ if (security.sessionVerified !== true || typeof principalSessionId !== 'string' || principalSessionId.length === 0) {
314
+ return { reason: 'SESSION_AUTH_REQUIRED', refs: [...refs, ruleSource('ACTION_SESSION_VERIFIED')] };
315
+ }
316
+ const binding = actionBinding(store, task);
317
+ if (!binding || binding.role_type !== 'SUPERVISOR' || binding.status !== 'ACTIVE') {
318
+ return {
319
+ reason: 'TERRITORY_SUPERVISOR_MISSING',
320
+ refs: [...refs, tableSource('territories', task.territory_id), tableSource('role_bindings', binding?.binding_id), ruleSource('ACTION_TERRITORY_SUPERVISOR')],
321
+ };
322
+ }
323
+ if (binding.session_id === null || binding.session_id !== principalSessionId) {
324
+ return {
325
+ reason: 'SESSION_AUTH_REQUIRED',
326
+ refs: [...refs, tableSource('role_bindings', binding.binding_id), ruleSource('ACTION_SUPERVISOR_SESSION_MATCH')],
327
+ };
328
+ }
329
+ const scope = security.scope ?? [];
330
+ if (!scope.includes(task.territory_id) && !scope.includes(`territory:${task.territory_id}`)) {
331
+ return { reason: 'ROLE_SCOPE_REQUIRED', refs: [...refs, tableSource('territories', task.territory_id), ruleSource('ACTION_TERRITORY_SCOPE')] };
332
+ }
333
+ if (security.hostContext !== true) {
334
+ return { reason: 'HOST_CONTEXT_REQUIRED', refs: [...refs, ruleSource('ACTION_HOST_CONTEXT')] };
335
+ }
336
+ if (!(security.commandCoverage ?? []).includes(commandForAction(action))) {
337
+ return { reason: 'COMMAND_UNAVAILABLE', refs: [...refs, ruleSource('ACTION_COMMAND_COVERAGE')] };
338
+ }
339
+ return null;
340
+ }
341
+ /** 只返回生命周期候选的可执行性,不执行 action,也不改变任何状态。 */
342
+ export function buildActionAvailability(store, task, execution, security = {}) {
343
+ const startBlocked = isGovernedStartBlocked(execution);
344
+ const lifecycleActions = allowedActionsFor(task, execution);
345
+ const actions = startBlocked && !lifecycleActions.includes('start')
346
+ ? ['start', ...lifecycleActions]
347
+ : lifecycleActions;
348
+ return actions.map((action) => {
349
+ const refs = [tableSource('tasks', task.task_id)];
350
+ if (action === 'start' && startBlocked && execution) {
351
+ const blockedRefs = [
352
+ ...refs,
353
+ tableSource('executions', execution.execution_id),
354
+ ...(execution.lease_id ? [tableSource('execution_leases', execution.lease_id)] : []),
355
+ ruleSource('START_BLOCKED_BY_UNSETTLED_GOVERNED_EXECUTION'),
356
+ ];
357
+ return {
358
+ action,
359
+ lifecycleAllowed: false,
360
+ executable: false,
361
+ disabledReason: attentionReason(execution.state === 'RECOVERING' ? 'EXECUTION_RECOVERING' : 'ILLEGAL_EXECUTION_STATE', blockedRefs),
362
+ sourceRefs: boundedSourceRefs(blockedRefs),
363
+ };
364
+ }
365
+ if (OWNER_ONLY_ACTIONS.includes(action)) {
366
+ return {
367
+ action,
368
+ lifecycleAllowed: true,
369
+ executable: false,
370
+ disabledReason: attentionReason('DIRECT_SLASH_REQUIRED', [...refs, ruleSource('OWNER_DIRECT_SLASH_ONLY')]),
371
+ sourceRefs: boundedSourceRefs([...refs, ruleSource('OWNER_DIRECT_SLASH_ONLY')]),
372
+ };
373
+ }
374
+ const missing = firstMissingActionContext(store, task, action, security);
375
+ if (missing) {
376
+ return {
377
+ action,
378
+ lifecycleAllowed: true,
379
+ executable: false,
380
+ disabledReason: attentionReason(missing.reason, missing.refs),
381
+ sourceRefs: boundedSourceRefs(missing.refs),
382
+ };
383
+ }
384
+ if (action.startsWith('execution:') && execution?.execution_contract === 'GOVERNED_PERSISTENT') {
385
+ const unavailableRefs = [
386
+ ...refs,
387
+ tableSource('executions', execution.execution_id),
388
+ ...(execution.lease_id ? [tableSource('execution_leases', execution.lease_id)] : []),
389
+ ruleSource('GOVERNED_RUNTIME_CONTROL_UNAVAILABLE'),
390
+ ];
391
+ return {
392
+ action,
393
+ lifecycleAllowed: true,
394
+ executable: false,
395
+ disabledReason: attentionReason('GOVERNED_RUNTIME_CONTROL_UNAVAILABLE', unavailableRefs),
396
+ sourceRefs: boundedSourceRefs(unavailableRefs),
397
+ };
398
+ }
399
+ return {
400
+ action,
401
+ lifecycleAllowed: true,
402
+ executable: true,
403
+ disabledReason: null,
404
+ sourceRefs: boundedSourceRefs([...refs, ruleSource('ACTION_CONTEXT_VERIFIED')]),
405
+ };
406
+ });
407
+ }
20
408
  export function toEventView(row) {
21
409
  return {
22
410
  seq: row.seq,
@@ -26,7 +414,7 @@ export function toEventView(row) {
26
414
  actorId: row.actor_id,
27
415
  targetType: row.target_type,
28
416
  targetId: row.target_id,
29
- payload: parseJson(row.payload_json),
417
+ payload: sanitizePublicJsonObject(parseJson(row.payload_json)),
30
418
  createdAt: row.created_at,
31
419
  };
32
420
  }
@@ -39,43 +427,105 @@ export function toBindingView(row) {
39
427
  runtimeType: row.runtime_type,
40
428
  sessionDisplay: maskSessionId(row.session_id),
41
429
  sessionBound: Boolean(row.session_id),
42
- modelName: row.model_name,
43
- agentName: row.agent_name,
44
- sessionMeta: row.session_meta ? parseJson(row.session_meta) : null,
45
- executionProfile: profile ? { provider: profile.provider ?? null, model: profile.model ?? null } : null,
430
+ modelName: row.model_name === null ? null : sanitizePublicJsonString(row.model_name, 'private-config'),
431
+ agentName: row.agent_name === null ? null : sanitizePublicJsonString(row.agent_name, 'private-config'),
432
+ sessionMeta: row.session_meta ? sanitizePublicJsonObject(parseJson(row.session_meta)) : null,
433
+ executionProfile: profile ? {
434
+ provider: profile.provider === undefined ? null : sanitizePublicJsonString(profile.provider, 'private-config'),
435
+ model: profile.model === undefined ? null : sanitizePublicJsonString(profile.model, 'private-config'),
436
+ } : null,
46
437
  createdAt: row.created_at,
47
438
  };
48
439
  }
49
440
  /**
50
- * v0.5.2(M1-B/P0-C):会话标识脱敏——普通快照只暴露尾部(如 `…8f21`),
51
- * 完整 session id 仅保留在审计事件面(events payload),避免身份标识被 UI 面扩散。
441
+ * v0.5.2(M1-B/P0-C):会话标识脱敏——公共 GUI 读面只暴露尾部(如 `…8f21`)。
442
+ * Event payload 和 Binding session metadata 也必须经过递归有界脱敏。
52
443
  */
53
444
  function maskSessionId(id) {
54
445
  if (!id)
55
446
  return null;
56
- return id.length > 8 ? `…${id.slice(-8)}` : `…${id}`;
447
+ return id.length > 8 ? `…${id.slice(-8)}` : REDACTED_JSON_VALUE;
57
448
  }
58
449
  export function toTerritoryView(row) {
59
450
  return {
60
451
  territoryId: row.territory_id,
61
452
  name: row.name,
62
- workspacePath: row.workspace_path,
63
- summary: row.summary,
453
+ workspacePath: row.workspace_path === null ? null : sanitizePublicJsonString(row.workspace_path),
454
+ summary: row.summary === null ? null : sanitizePublicJsonString(row.summary),
64
455
  status: row.status,
65
456
  createdAt: row.created_at,
66
457
  };
67
458
  }
459
+ function toTaskAssignmentHistoryView(row) {
460
+ return {
461
+ assignmentId: publicReferenceId(row.assignment_id) ?? 'unknown',
462
+ workerBindingId: publicReferenceId(row.worker_binding_id) ?? 'unknown',
463
+ assignedByBindingId: publicReferenceId(row.assigned_by) ?? 'unknown',
464
+ assignedAt: safeDate(row.assigned_at),
465
+ endedAt: safeDate(row.ended_at),
466
+ endReason: row.end_reason ? boundedText(row.end_reason) || null : null,
467
+ previousAssignmentId: publicReferenceId(row.previous_assignment_id),
468
+ handoffReason: row.handoff_reason ? boundedText(row.handoff_reason) || null : null,
469
+ sourceRefs: boundedSourceRefs([tableSource('task_assignments', row.assignment_id)]),
470
+ };
471
+ }
472
+ function toSupervisorDecisionView(row) {
473
+ const payload = parseJson(row.payload_json);
474
+ const payloadReference = (value) => typeof value === 'string' ? publicReferenceId(value) : null;
475
+ const payloadText = (value) => {
476
+ if (typeof value !== 'string')
477
+ return null;
478
+ return boundedText(value) || null;
479
+ };
480
+ const handoff = row.event_type === 'TASK_HANDED_OFF';
481
+ const eventSupervisorId = row.actor_role === 'SUPERVISOR' ? publicReferenceId(row.actor_id) : null;
482
+ const reviewerBindingId = handoff
483
+ ? eventSupervisorId
484
+ : payloadReference(payload.reviewer_binding_id) ?? eventSupervisorId;
485
+ const fromAssignmentId = handoff ? payloadReference(payload.from_assignment_id) : null;
486
+ const fromWorkerBindingId = handoff ? payloadReference(payload.from_worker_binding_id) : null;
487
+ const toAssignmentId = handoff ? payloadReference(payload.to_assignment_id) : null;
488
+ const toWorkerBindingId = handoff ? payloadReference(payload.to_worker_binding_id) : null;
489
+ const reviewedAttemptNo = typeof payload.reviewed_attempt_no === 'number'
490
+ && Number.isSafeInteger(payload.reviewed_attempt_no)
491
+ && payload.reviewed_attempt_no >= 0
492
+ ? payload.reviewed_attempt_no
493
+ : null;
494
+ const refs = [
495
+ eventSource(row.seq),
496
+ tableSource('tasks', row.target_id),
497
+ ...(reviewerBindingId ? [tableSource('role_bindings', reviewerBindingId)] : []),
498
+ ...(fromAssignmentId ? [tableSource('task_assignments', fromAssignmentId)] : []),
499
+ ...(fromWorkerBindingId ? [tableSource('role_bindings', fromWorkerBindingId)] : []),
500
+ ...(toAssignmentId ? [tableSource('task_assignments', toAssignmentId)] : []),
501
+ ...(toWorkerBindingId ? [tableSource('role_bindings', toWorkerBindingId)] : []),
502
+ ];
503
+ return {
504
+ seq: row.seq,
505
+ decision: handoff ? 'HANDOFF' : payloadText(payload.decision) ?? (boundedText(row.event_type) || 'UNKNOWN'),
506
+ reason: payloadText(handoff ? payload.handoff_reason : payload.reason),
507
+ reviewerBindingId,
508
+ reviewedAttemptNo,
509
+ claimedOutcome: handoff ? null : payloadText(payload.claimed_outcome),
510
+ fromAssignmentId,
511
+ fromWorkerBindingId,
512
+ toAssignmentId,
513
+ toWorkerBindingId,
514
+ sourceRefs: boundedSourceRefs(refs),
515
+ createdAt: row.created_at,
516
+ };
517
+ }
68
518
  export function toClaimView(row) {
69
519
  const payload = parseJson(row.result_json);
70
520
  return {
71
521
  resultId: row.result_id,
72
522
  attemptNo: row.attempt_no,
73
523
  workerBindingId: row.worker_binding_id,
74
- sessionId: row.session_id,
524
+ sessionId: maskSessionId(row.session_id),
75
525
  claimedOutcome: row.outcome,
76
- summary: typeof payload.summary === 'string' ? payload.summary : null,
77
- artifacts: stringList(payload.artifacts),
78
- risks: stringList(payload.risks),
526
+ summary: typeof payload.summary === 'string' ? sanitizePublicJsonString(payload.summary) : null,
527
+ artifacts: sanitizePublicStringList(payload.artifacts),
528
+ risks: sanitizePublicStringList(payload.risks),
79
529
  createdAt: row.created_at,
80
530
  };
81
531
  }
@@ -85,9 +535,9 @@ export function toExecutionView(row) {
85
535
  taskId: row.task_id,
86
536
  attemptNo: row.attempt_no,
87
537
  workerBindingId: row.worker_binding_id,
88
- sessionId: row.session_id,
538
+ sessionId: maskSessionId(row.session_id),
89
539
  state: row.state,
90
- detail: row.detail,
540
+ detail: row.detail === null ? null : sanitizePublicJsonString(row.detail),
91
541
  startedAt: row.started_at,
92
542
  heartbeatAt: row.heartbeat_at,
93
543
  endedAt: row.ended_at,
@@ -142,8 +592,9 @@ function toDecisionView(row) {
142
592
  createdAt: row.created_at,
143
593
  };
144
594
  }
145
- function toDispatchView(row) {
595
+ function toDispatchView(row, store) {
146
596
  return {
597
+ usage: readDispatchUsage(store, row.dispatch_id),
147
598
  dispatchId: row.dispatch_id,
148
599
  leaseId: row.lease_id,
149
600
  executionId: row.execution_id,
@@ -165,7 +616,7 @@ export function buildGovernance(store, kingdomId) {
165
616
  workerSessions: store.listAffinities(kingdomId).map(toAffinityView),
166
617
  leases: store.listLeases(kingdomId).map(toLeaseView),
167
618
  decisions: store.listCapabilityDecisions(kingdomId).map(toDecisionView),
168
- dispatches: store.listDispatches(kingdomId).map(toDispatchView),
619
+ dispatches: store.listDispatches(kingdomId).map(row => toDispatchView(row, store)),
169
620
  };
170
621
  }
171
622
  /** 任务级 Runtime Governance 投影(TaskDetail 用)。 */
@@ -176,7 +627,7 @@ export function buildTaskGovernance(store, taskId) {
176
627
  workerSessions: [],
177
628
  leases: store.listLeases(store.getDefaultKingdom()?.kingdom_id ?? '').filter(l => l.task_id === taskId).map(toLeaseView),
178
629
  decisions: store.listCapabilityDecisions(store.getDefaultKingdom()?.kingdom_id ?? '').filter(d => d.task_id === taskId).map(toDecisionView),
179
- dispatches: store.listDispatches(store.getDefaultKingdom()?.kingdom_id ?? '').filter(d => d.task_id === taskId).map(toDispatchView),
630
+ dispatches: store.listDispatches(store.getDefaultKingdom()?.kingdom_id ?? '').filter(d => d.task_id === taskId).map(row => toDispatchView(row, store)),
180
631
  };
181
632
  }
182
633
  /**
@@ -187,27 +638,33 @@ export function buildTaskGovernance(store, taskId) {
187
638
  */
188
639
  export function allowedActionsFor(task, execution) {
189
640
  const status = asTaskStatus(task.status);
190
- const live = execution !== null && isLiveExecutionState(asExecutionState(execution.state));
641
+ const executionState = execution ? asExecutionState(execution.state) : null;
642
+ const live = executionState !== null && isLiveExecutionState(executionState);
191
643
  switch (status) {
192
644
  case 'CREATED':
193
645
  return ['assign'];
194
646
  case 'ASSIGNED':
195
- return ['start'];
647
+ return executionState === 'RECOVERING' && execution?.execution_contract === 'GOVERNED_PERSISTENT' ? ['reconcile'] : isGovernedStartBlocked(execution) ? [] : ['start'];
196
648
  case 'RUNNING': {
197
- if (!live) {
649
+ // RECOVERING means the Runtime outcome is unknown. Exposing start here would
650
+ // invite a duplicate attempt before reconciliation, so the projection fails closed.
651
+ if (executionState === 'RECOVERING')
652
+ return execution?.execution_contract === 'GOVERNED_PERSISTENT' ? ['reconcile'] : [];
653
+ if (execution === null || !live) {
198
654
  // REWORK 之后:任务已回 RUNNING,但还没有新的 Execution。
199
655
  return ['start'];
200
656
  }
201
- const state = asExecutionState(execution.state);
202
657
  const actions = ['execution:abort'];
203
- if (state === 'PAUSED')
658
+ if (executionState === 'PAUSED')
204
659
  actions.unshift('execution:resume');
205
660
  else if (execution.pause_requested_at === null)
206
661
  actions.unshift('execution:pause');
662
+ else if (execution.execution_contract === 'LEGACY_COMPAT')
663
+ actions.unshift('execution:resume');
207
664
  return actions;
208
665
  }
209
666
  case 'REVIEW':
210
- return ['review:accept', 'review:rework', 'review:fail'];
667
+ return ['review:accept', 'review:rework', 'review:fail', 'review:handoff'];
211
668
  default:
212
669
  return [];
213
670
  }
@@ -223,7 +680,7 @@ export function toTaskView(store, task) {
223
680
  acceptanceCriteria: task.acceptance_criteria,
224
681
  status: task.status,
225
682
  assignedBindingId: task.assigned_binding_id,
226
- resultSummary: task.result_summary,
683
+ resultSummary: task.result_summary === null ? null : sanitizePublicJsonString(task.result_summary),
227
684
  attemptCount: store.maxAttemptNo(task.task_id),
228
685
  latestClaim: claim ? toClaimView(claim) : null,
229
686
  latestExecution: execution ? toExecutionView(execution) : null,
@@ -232,17 +689,94 @@ export function toTaskView(store, task) {
232
689
  updatedAt: task.updated_at,
233
690
  };
234
691
  }
692
+ function eventBindingId(input, event, role) {
693
+ const payload = parseJson(event.payload_json);
694
+ const payloadBindingId = (key) => {
695
+ const value = payload[key];
696
+ return typeof value === 'string' ? value : null;
697
+ };
698
+ if (role === 'WORKER') {
699
+ const explicit = payloadBindingId('worker_binding_id')
700
+ ?? payloadBindingId('to_worker_binding_id');
701
+ if (explicit)
702
+ return explicit;
703
+ // TASK_ACCEPTED is emitted by the Supervisor and carries no worker field;
704
+ // the task's current assigned_binding_id is the exact binding relation for
705
+ // that task. Do not use Task.RUNNING as a live execution signal here.
706
+ if (event.event_type === 'TASK_ACCEPTED') {
707
+ return input.tasks.find(task => task.task_id === event.target_id)?.assigned_binding_id ?? null;
708
+ }
709
+ return event.actor_role === 'WORKER' ? event.actor_id : null;
710
+ }
711
+ if (role === 'SUPERVISOR') {
712
+ return event.actor_role === 'SUPERVISOR'
713
+ ? event.actor_id
714
+ : payloadBindingId('reviewer_binding_id') ?? payloadBindingId('supervisor_binding_id');
715
+ }
716
+ if (role === 'CHANCELLOR') {
717
+ return event.actor_role === 'CHANCELLOR'
718
+ ? event.actor_id
719
+ : payloadBindingId('chancellor_binding_id');
720
+ }
721
+ return event.actor_role === 'OWNER' ? event.actor_id : null;
722
+ }
723
+ function activeBindingFor(input, bindingId) {
724
+ return input.bindings.find(binding => binding.binding_id === bindingId && binding.status === 'ACTIVE') ?? null;
725
+ }
726
+ function eventTask(input, event) {
727
+ if (!event.target_id)
728
+ return null;
729
+ if (event.target_type !== null && event.target_type !== 'task')
730
+ return null;
731
+ return input.tasks.find(task => task.task_id === event.target_id) ?? null;
732
+ }
235
733
  /**
236
- * 在窗口期内找出**最近的一条**指定类型事件(events 已按 seq 降序传入)。
237
- *
238
- * 注意语义:返回的是这些类型里**最新的那条**,若它已过期则返回 null
239
- * ——而不是"往前翻到第一条没过期的"。因此调用方必须把一组互斥的候选类型
240
- * 一次性传进来,再按 `event_type` 分派;分成多次调用会让较早的动作
241
- * 盖住较晚的动作(例如刚 ACCEPT 完却还在播派发)。
734
+ * A transient event is usable only when its actor, role, task assignment, and
735
+ * territory scope all agree. This prevents one recent event from broadcasting
736
+ * to every Supervisor/Worker in the kingdom.
242
737
  */
243
- function latestWithin(input, types) {
738
+ function eventMatchesBinding(input, event, role, bindingId) {
739
+ const binding = activeBindingFor(input, bindingId);
740
+ if (!binding || binding.role_type !== role)
741
+ return false;
742
+ if (eventBindingId(input, event, role) !== bindingId)
743
+ return false;
744
+ if (role === 'OWNER')
745
+ return true;
746
+ const task = eventTask(input, event);
747
+ if (!task)
748
+ return false;
749
+ const territory = input.territories.find(item => item.territory_id === task.territory_id);
750
+ if (!territory)
751
+ return false;
752
+ if (role === 'CHANCELLOR') {
753
+ return event.actor_role === 'CHANCELLOR';
754
+ }
755
+ if (role === 'SUPERVISOR') {
756
+ return territory.supervisor_binding_id === bindingId
757
+ && event.actor_role === 'SUPERVISOR';
758
+ }
759
+ // Worker transient evidence must point to the task's exact current
760
+ // assignment. The canonical WORKER_EXECUTION_FAILED event is emitted by
761
+ // the Supervisor and carries the affected Worker in payload, so both actor
762
+ // scope and payload binding must agree.
763
+ if (task.assigned_binding_id !== bindingId)
764
+ return false;
765
+ if (event.event_type === 'WORKER_EXECUTION_FAILED') {
766
+ if (event.actor_role !== 'SUPERVISOR' || !event.actor_id)
767
+ return false;
768
+ const supervisorBinding = activeBindingFor(input, event.actor_id);
769
+ return supervisorBinding?.role_type === 'SUPERVISOR'
770
+ && supervisorBinding.binding_id === territory.supervisor_binding_id;
771
+ }
772
+ if (event.event_type === 'TASK_ASSIGNED' || event.event_type === 'TASK_ACCEPTED') {
773
+ return event.actor_role === 'SUPERVISOR' && territory.supervisor_binding_id === event.actor_id;
774
+ }
775
+ return event.actor_role === 'WORKER';
776
+ }
777
+ function latestWithinFor(input, types, role, bindingId) {
244
778
  for (const event of input.events) {
245
- if (!types.includes(event.event_type))
779
+ if (!types.includes(event.event_type) || !eventMatchesBinding(input, event, role, bindingId))
246
780
  continue;
247
781
  const age = input.nowMs - Date.parse(event.created_at);
248
782
  if (Number.isNaN(age))
@@ -266,6 +800,7 @@ function actor(role, binding, state, activity, extra = {}) {
266
800
  remainingMs: null,
267
801
  fallbackState: 'idle',
268
802
  sourceSeq: null,
803
+ indeterminate: false,
269
804
  ...extra,
270
805
  };
271
806
  }
@@ -285,7 +820,7 @@ function transientFrom(input, event) {
285
820
  function chancellorActor(input, binding) {
286
821
  if (!binding)
287
822
  return actor('CHANCELLOR', null, 'absent', null);
288
- const planned = latestWithin(input, ['TASK_PLANNED']);
823
+ const planned = latestWithinFor(input, ['TASK_PLANNED'], 'CHANCELLOR', binding.binding_id);
289
824
  if (planned) {
290
825
  return actor('CHANCELLOR', binding, 'planning', 'plan', {
291
826
  taskId: planned.target_id,
@@ -305,7 +840,7 @@ function chancellorActor(input, binding) {
305
840
  function supervisorActor(input, binding) {
306
841
  if (!binding)
307
842
  return actor('SUPERVISOR', null, 'absent', null);
308
- const recent = latestWithin(input, ['TASK_ASSIGNED', 'TASK_REWORK_REQUESTED', 'TASK_ACCEPTED', 'TASK_FAILED']);
843
+ const recent = latestWithinFor(input, ['TASK_ASSIGNED', 'TASK_REWORK_REQUESTED', 'TASK_ACCEPTED', 'TASK_FAILED'], 'SUPERVISOR', binding.binding_id);
309
844
  if (recent) {
310
845
  const common = { taskId: recent.target_id, fallbackState: 'idle', ...transientFrom(input, recent) };
311
846
  switch (recent.event_type) {
@@ -320,7 +855,12 @@ function supervisorActor(input, binding) {
320
855
  }
321
856
  }
322
857
  // 持续状态:有待审 Claim 就一直在复核。
323
- const pending = input.tasks.find(t => t.status === 'REVIEW');
858
+ const pending = input.tasks.find(task => {
859
+ if (task.status !== 'REVIEW')
860
+ return false;
861
+ const territory = input.territories.find(item => item.territory_id === task.territory_id);
862
+ return territory?.supervisor_binding_id === binding.binding_id;
863
+ });
324
864
  if (pending) {
325
865
  return actor('SUPERVISOR', binding, 'reviewing', 'review', {
326
866
  taskId: pending.task_id,
@@ -339,7 +879,25 @@ function supervisorActor(input, binding) {
339
879
  function workerActor(input, binding) {
340
880
  if (!binding)
341
881
  return actor('WORKER', null, 'absent', null);
342
- const live = input.executions.find(e => isLiveExecutionState(asExecutionState(e.state)));
882
+ const territoryIds = new Set(input.tasks
883
+ .filter(task => task.assigned_binding_id === binding.binding_id)
884
+ .map(task => task.territory_id)
885
+ .filter(territoryId => input.territories.some(territory => territory.territory_id === territoryId)));
886
+ const territoryIndeterminate = territoryIds.size !== 1;
887
+ const liveExecutions = input.executions.filter(execution => {
888
+ if (execution.worker_binding_id !== binding.binding_id || !isLiveExecutionState(asExecutionState(execution.state)))
889
+ return false;
890
+ const task = input.tasks.find(item => item.task_id === execution.task_id);
891
+ return task?.assigned_binding_id === binding.binding_id
892
+ && input.territories.some(territory => territory.territory_id === task.territory_id);
893
+ });
894
+ if (liveExecutions.length > 1) {
895
+ return actor('WORKER', binding, 'confused', null, {
896
+ fallbackState: 'idle',
897
+ indeterminate: true,
898
+ });
899
+ }
900
+ const live = liveExecutions[0] ?? null;
343
901
  if (live) {
344
902
  const state = asExecutionState(live.state);
345
903
  const common = {
@@ -347,6 +905,7 @@ function workerActor(input, binding) {
347
905
  executionId: live.execution_id,
348
906
  attemptNo: live.attempt_no,
349
907
  since: live.started_at,
908
+ indeterminate: territoryIndeterminate,
350
909
  };
351
910
  if (state === 'PAUSED') {
352
911
  return actor('WORKER', binding, 'sleeping', null, { ...common, fallbackState: 'sleeping' });
@@ -357,7 +916,7 @@ function workerActor(input, binding) {
357
916
  }
358
917
  // 一次性表演:庆祝(任务刚被 ACCEPT)与困惑(宿主观察到执行没跑起来)。
359
918
  // 同样一次性取最新再分派,避免旧事件盖住新事件。
360
- const recent = latestWithin(input, ['TASK_ACCEPTED', 'WORKER_EXECUTION_FAILED']);
919
+ const recent = latestWithinFor(input, ['TASK_ACCEPTED', 'WORKER_EXECUTION_FAILED'], 'WORKER', binding.binding_id);
361
920
  if (recent) {
362
921
  const common = { taskId: recent.target_id, fallbackState: 'idle', ...transientFrom(input, recent) };
363
922
  return recent.event_type === 'TASK_ACCEPTED'
@@ -370,21 +929,734 @@ function workerActor(input, binding) {
370
929
  return actor('WORKER', binding, 'waiting', null, {
371
930
  taskId: awaiting.task_id,
372
931
  since: awaiting.updated_at,
932
+ indeterminate: territoryIndeterminate,
373
933
  });
374
934
  }
375
- return actor('WORKER', binding, 'idle', null);
935
+ return actor('WORKER', binding, 'idle', null, { indeterminate: territoryIndeterminate });
376
936
  }
377
937
  /** 计算全部角色此刻的表演语义。 */
378
938
  export function projectStage(input) {
379
- const byRole = (role) => input.bindings.find(b => b.role_type === role) ?? null;
380
- const owner = byRole('OWNER');
939
+ const bindingsFor = (role) => input.bindings.filter(binding => binding.role_type === role && binding.status === 'ACTIVE');
940
+ const withAbsentFallback = (role, bindings, project) => bindings.length > 0
941
+ ? bindings.map(project)
942
+ : [project(null)];
381
943
  return [
382
- owner ? actor('OWNER', owner, 'idle', null) : actor('OWNER', null, 'absent', null),
383
- chancellorActor(input, byRole('CHANCELLOR')),
384
- supervisorActor(input, byRole('SUPERVISOR')),
385
- workerActor(input, byRole('WORKER')),
944
+ ...withAbsentFallback('OWNER', bindingsFor('OWNER'), binding => binding ? actor('OWNER', binding, 'idle', null) : actor('OWNER', null, 'absent', null)),
945
+ ...withAbsentFallback('CHANCELLOR', bindingsFor('CHANCELLOR'), binding => chancellorActor(input, binding)),
946
+ ...withAbsentFallback('SUPERVISOR', bindingsFor('SUPERVISOR'), binding => supervisorActor(input, binding)),
947
+ ...withAbsentFallback('WORKER', bindingsFor('WORKER'), binding => workerActor(input, binding)),
386
948
  ];
387
949
  }
950
+ function attentionItem(id, severity, entityRef, code, summary, refs) {
951
+ const reason = attentionReason(code, refs);
952
+ return {
953
+ id,
954
+ severity,
955
+ entityRef,
956
+ reason,
957
+ summary: boundedText(summary),
958
+ sourceRefs: reason.sourceRefs,
959
+ };
960
+ }
961
+ function claimExecutionMismatch(claim, execution) {
962
+ const expected = claim.outcome === 'COMPLETED' || claim.outcome === 'FAILED' ? claim.outcome : null;
963
+ return expected !== null
964
+ && execution.attempt_no === claim.attempt_no
965
+ && executionTerminality(execution.state) === 'TERMINAL'
966
+ && execution.state !== expected;
967
+ }
968
+ /** 固定 reason code 的 Attention 派生;只读,不修复、不重试、不推进状态。 */
969
+ export function buildAttention(store, kingdomId) {
970
+ const kingdom = store.getDefaultKingdom();
971
+ if (!kingdom || kingdom.kingdom_id !== kingdomId) {
972
+ return [attentionItem('kingdom:missing', 'UNKNOWN', null, 'CONFIGURATION_INCOMPLETE', 'Kingdom is not initialized; projection remains UNKNOWN', [ruleSource('KINGDOM_PRESENT')])];
973
+ }
974
+ const items = [];
975
+ const tasks = store.listTasks(kingdomId);
976
+ for (const task of tasks) {
977
+ const claim = store.latestWorkerResult(task.task_id);
978
+ const execution = store.latestExecution(task.task_id);
979
+ if (claim && execution && claimExecutionMismatch(claim, execution)) {
980
+ items.push(attentionItem(timelineId('mismatch', task.task_id), 'CRITICAL', publicEntityRef('task', task.task_id), 'CLAIM_EXECUTION_MISMATCH', `Claim ${boundedText(claim.outcome)} does not match terminal Execution ${boundedText(execution.state)}`, [tableSource('worker_results', claim.result_id), tableSource('executions', execution.execution_id)]));
981
+ }
982
+ if (execution?.state === 'RECOVERING') {
983
+ items.push(attentionItem(timelineId('execution-recovering', execution.execution_id), 'UNKNOWN', publicEntityRef('task', task.task_id), 'EXECUTION_RECOVERING', 'Execution is RECOVERING; reconcile Runtime evidence before any new attempt', [tableSource('executions', execution.execution_id), ruleSource('RECOVERING_FAIL_CLOSED')]));
984
+ }
985
+ }
986
+ // v4 是只读地标;非 v4 不访问这些表,也不伪造治理状态。
987
+ if (store.isSchemaV4) {
988
+ for (const decision of store.listCapabilityDecisions(kingdomId)) {
989
+ if (decision.decision !== 'DENIED')
990
+ continue;
991
+ items.push(attentionItem(timelineId('decision', decision.decision_id), 'ATTENTION', publicEntityRef('task', decision.task_id), 'CAPABILITY_DENIED', `Capability decision DENIED${decision.reason_code ? ` (${boundedText(decision.reason_code)})` : ''}`, [tableSource('capability_decisions', decision.decision_id)]));
992
+ }
993
+ for (const lease of store.listLeases(kingdomId)) {
994
+ if (lease.state !== 'RECOVERING' && !(lease.released_at === null && lease.state === 'TERMINAL'))
995
+ continue;
996
+ items.push(attentionItem(timelineId('lease', lease.lease_id), 'UNKNOWN', publicEntityRef('task', lease.task_id), 'LEASE_NOT_RELEASED', `Lease ${boundedText(lease.state)} has no confirmed release`, [tableSource('execution_leases', lease.lease_id)]));
997
+ }
998
+ for (const dispatch of store.listDispatches(kingdomId)) {
999
+ if (dispatch.state === 'RECOVERING') {
1000
+ items.push(attentionItem(timelineId('dispatch', dispatch.dispatch_id), 'UNKNOWN', publicEntityRef('task', dispatch.task_id), 'LEASE_NOT_RELEASED', 'Dispatch is RECOVERING; no automatic retry is inferred', [tableSource('dispatch_records', dispatch.dispatch_id)]));
1001
+ }
1002
+ else if (dispatch.receipt_json !== null && !dispatch.terminal_evidence_json
1003
+ && dispatch.state !== 'TERMINAL' && dispatch.state !== 'FAILED') {
1004
+ items.push(attentionItem(timelineId('terminal', dispatch.dispatch_id), 'UNKNOWN', publicEntityRef('task', dispatch.task_id), 'TERMINAL_EVIDENCE_MISSING', 'Dispatch receipt exists but terminal evidence is missing', [tableSource('dispatch_records', dispatch.dispatch_id), ruleSource('TERMINAL_EVIDENCE_REQUIRED')]));
1005
+ }
1006
+ }
1007
+ }
1008
+ return items.slice(0, MAX_PROJECTION_TIMELINE_ITEMS);
1009
+ }
1010
+ function timelineAttention(attentions, entityRef) {
1011
+ if (!entityRef)
1012
+ return null;
1013
+ return attentions.find(item => item.entityRef?.id === entityRef.id)?.reason ?? null;
1014
+ }
1015
+ function timelineSourceRefKey(ref) {
1016
+ return JSON.stringify([ref.sourceType, ref.entityType, ref.entityId, ref.eventSeq, ref.ruleCode]);
1017
+ }
1018
+ function sortTimeline(items) {
1019
+ return [...items].sort((a, b) => {
1020
+ const aTime = a.occurredAt ? Date.parse(a.occurredAt) : Number.POSITIVE_INFINITY;
1021
+ const bTime = b.occurredAt ? Date.parse(b.occurredAt) : Number.POSITIVE_INFINITY;
1022
+ return (aTime - bTime) || a.id.localeCompare(b.id);
1023
+ });
1024
+ }
1025
+ function mismatchEvidenceTime(explanation, items) {
1026
+ const sourceKeys = new Set(explanation.sourceRefs
1027
+ .filter(ref => ref.sourceType === 'table-row'
1028
+ && (ref.entityType === 'worker_results' || ref.entityType === 'executions'))
1029
+ .map(timelineSourceRefKey));
1030
+ const evidenceTimes = items
1031
+ .filter(item => item.id !== explanation.id
1032
+ && item.sourceRefs.some(ref => sourceKeys.has(timelineSourceRefKey(ref))))
1033
+ .map(item => item.occurredAt ? Date.parse(item.occurredAt) : Number.NaN)
1034
+ .filter(Number.isFinite);
1035
+ return evidenceTimes.length > 0 ? Math.max(...evidenceTimes) : Number.NEGATIVE_INFINITY;
1036
+ }
1037
+ function mismatchEvidenceGroups(items) {
1038
+ const ordered = sortTimeline(items);
1039
+ const newestFirst = [...ordered].reverse();
1040
+ return newestFirst
1041
+ .filter(item => item.kind === 'DERIVED_EXPLANATION'
1042
+ && item.attentionReason?.code === 'CLAIM_EXECUTION_MISMATCH')
1043
+ .map(explanation => {
1044
+ const sourceKeys = new Set(explanation.sourceRefs
1045
+ .filter(ref => ref.sourceType === 'table-row'
1046
+ && (ref.entityType === 'worker_results' || ref.entityType === 'executions'))
1047
+ .map(timelineSourceRefKey));
1048
+ const related = newestFirst.filter(item => item.id !== explanation.id
1049
+ && item.sourceRefs.some(ref => sourceKeys.has(timelineSourceRefKey(ref))));
1050
+ return {
1051
+ explanation,
1052
+ group: [explanation, ...related],
1053
+ evidenceTime: mismatchEvidenceTime(explanation, items),
1054
+ };
1055
+ })
1056
+ .sort((a, b) => (b.evidenceTime - a.evidenceTime)
1057
+ // ID is only a deterministic tie-break after actual evidence time; it never represents recency.
1058
+ || a.explanation.id.localeCompare(b.explanation.id))
1059
+ .map(entry => entry.group);
1060
+ }
1061
+ function isMismatchExplanation(item) {
1062
+ return item.kind === 'DERIVED_EXPLANATION'
1063
+ && item.attentionReason?.code === 'CLAIM_EXECUTION_MISMATCH';
1064
+ }
1065
+ /** 超限时保留最新证据;mismatch 的解释与其 Claim/Execution 成组保留。 */
1066
+ function retainTimelineEvidence(items) {
1067
+ const ordered = sortTimeline(items);
1068
+ if (ordered.length <= MAX_PROJECTION_TIMELINE_ITEMS)
1069
+ return ordered;
1070
+ const selected = [];
1071
+ const selectedIds = new Set();
1072
+ const add = (item) => {
1073
+ if (selected.length >= MAX_PROJECTION_TIMELINE_ITEMS || selectedIds.has(item.id))
1074
+ return;
1075
+ selected.push(item);
1076
+ selectedIds.add(item.id);
1077
+ };
1078
+ const addNonMismatch = (item) => {
1079
+ // Mismatch explanations may only enter through addGroup(), never as an orphan.
1080
+ if (isMismatchExplanation(item))
1081
+ return;
1082
+ add(item);
1083
+ };
1084
+ const addGroup = (group) => {
1085
+ const unique = group.filter(item => !selectedIds.has(item.id));
1086
+ if (selected.length + unique.length > MAX_PROJECTION_TIMELINE_ITEMS)
1087
+ return;
1088
+ for (const item of unique)
1089
+ add(item);
1090
+ };
1091
+ const newestFirst = [...ordered].reverse();
1092
+ // A negative explanation is only traceable when its two source rows remain visible too.
1093
+ for (const group of mismatchEvidenceGroups(items))
1094
+ addGroup(group);
1095
+ // Keep each existing projection layer represented when it has candidates.
1096
+ for (const kind of ['GOVERNANCE_FACT', 'RUNTIME_OBSERVATION', 'WORKER_CLAIM', 'DERIVED_EXPLANATION']) {
1097
+ const newest = newestFirst.find(item => item.kind === kind);
1098
+ if (newest)
1099
+ addNonMismatch(newest);
1100
+ }
1101
+ // Fill the remaining bounded capacity from newest to oldest, then restore display order.
1102
+ for (const item of newestFirst)
1103
+ addNonMismatch(item);
1104
+ return sortTimeline(selected);
1105
+ }
1106
+ /** 从现有 row/event/observation 重建 timeline;不是新 Ledger。 */
1107
+ export function buildTimeline(store, kingdomId, options = {}) {
1108
+ const attentions = buildAttention(store, kingdomId);
1109
+ const items = [];
1110
+ const events = store.listEvents(kingdomId, Math.min(options.eventLimit ?? 50, MAX_PROJECTION_TIMELINE_ITEMS));
1111
+ for (const event of events) {
1112
+ const usageObservation = event.event_type === 'DISPATCH_USAGE_OBSERVED';
1113
+ const refs = [eventSource(event.seq)];
1114
+ const entityRef = event.target_type ? publicEntityRef(event.target_type, event.target_id) : null;
1115
+ items.push({
1116
+ id: `event:${event.seq}`,
1117
+ kind: usageObservation ? 'RUNTIME_OBSERVATION' : 'GOVERNANCE_FACT',
1118
+ occurredAt: safeDate(event.created_at),
1119
+ entityRef,
1120
+ authoritativeState: stateValue(event.event_type, usageObservation ? 'RUNTIME_OBSERVATION' : 'GOVERNANCE_FACT', refs),
1121
+ sourceRefs: boundedSourceRefs(refs),
1122
+ allowedActions: null,
1123
+ attentionReason: timelineAttention(attentions, entityRef),
1124
+ terminality: 'UNKNOWN',
1125
+ summary: usageObservation ? 'Provider usage observation (coverage may be partial)' : `Governance Fact: ${boundedText(event.event_type)}`,
1126
+ requiresOwnerAction: false,
1127
+ rawEvidenceAvailable: false,
1128
+ });
1129
+ }
1130
+ for (const task of store.listTasks(kingdomId)) {
1131
+ for (const claim of store.listWorkerResults(task.task_id).slice(-10)) {
1132
+ const refs = [tableSource('worker_results', claim.result_id)];
1133
+ const entityRef = publicEntityRef('task', task.task_id);
1134
+ items.push({
1135
+ id: timelineId('claim', claim.result_id),
1136
+ kind: 'WORKER_CLAIM',
1137
+ occurredAt: safeDate(claim.created_at),
1138
+ entityRef,
1139
+ authoritativeState: null,
1140
+ sourceRefs: boundedSourceRefs(refs),
1141
+ allowedActions: null,
1142
+ attentionReason: timelineAttention(attentions, entityRef),
1143
+ terminality: 'UNKNOWN',
1144
+ summary: `Worker Claim: ${boundedText(claim.outcome) || 'UNKNOWN'}`,
1145
+ requiresOwnerAction: false,
1146
+ rawEvidenceAvailable: false,
1147
+ });
1148
+ }
1149
+ for (const execution of store.listExecutions(task.task_id).slice(-10)) {
1150
+ const refs = [tableSource('executions', execution.execution_id)];
1151
+ const entityRef = publicEntityRef('execution', execution.execution_id);
1152
+ items.push({
1153
+ id: timelineId('execution', execution.execution_id),
1154
+ kind: 'RUNTIME_OBSERVATION',
1155
+ occurredAt: safeDate(execution.ended_at ?? execution.started_at),
1156
+ entityRef,
1157
+ authoritativeState: stateValue(execution.state, 'RUNTIME_OBSERVATION', refs),
1158
+ sourceRefs: boundedSourceRefs(refs),
1159
+ allowedActions: null,
1160
+ attentionReason: timelineAttention(attentions, publicEntityRef('task', task.task_id)),
1161
+ terminality: executionTerminality(execution.state),
1162
+ summary: `Runtime Observation: ${boundedText(execution.state)} (${boundedText(execution.execution_contract)})`,
1163
+ requiresOwnerAction: false,
1164
+ rawEvidenceAvailable: false,
1165
+ });
1166
+ }
1167
+ }
1168
+ if (store.isSchemaV4) {
1169
+ for (const decision of store.listCapabilityDecisions(kingdomId)) {
1170
+ const refs = [tableSource('capability_decisions', decision.decision_id)];
1171
+ items.push({
1172
+ id: timelineId('decision', decision.decision_id),
1173
+ kind: 'GOVERNANCE_FACT',
1174
+ occurredAt: safeDate(decision.created_at),
1175
+ entityRef: publicEntityRef('task', decision.task_id),
1176
+ authoritativeState: stateValue(`${decision.decision}/${decision.enforcement_status}`, 'GOVERNANCE_FACT', refs),
1177
+ sourceRefs: boundedSourceRefs(refs),
1178
+ allowedActions: null,
1179
+ attentionReason: timelineAttention(attentions, publicEntityRef('task', decision.task_id)),
1180
+ terminality: 'UNKNOWN',
1181
+ summary: `Governance Fact: Decision ${boundedText(decision.decision)}`,
1182
+ requiresOwnerAction: decision.decision === 'DENIED',
1183
+ rawEvidenceAvailable: decision.enforcement_evidence_json !== null,
1184
+ });
1185
+ }
1186
+ for (const lease of store.listLeases(kingdomId)) {
1187
+ const refs = [tableSource('execution_leases', lease.lease_id)];
1188
+ items.push({
1189
+ id: timelineId('lease', lease.lease_id),
1190
+ kind: 'GOVERNANCE_FACT',
1191
+ occurredAt: safeDate(lease.updated_at ?? lease.acquired_at),
1192
+ entityRef: publicEntityRef('task', lease.task_id),
1193
+ authoritativeState: stateValue(lease.state, 'GOVERNANCE_FACT', refs),
1194
+ sourceRefs: boundedSourceRefs(refs),
1195
+ allowedActions: null,
1196
+ attentionReason: timelineAttention(attentions, publicEntityRef('task', lease.task_id)),
1197
+ terminality: lease.state === 'RELEASED' ? 'TERMINAL' : lease.state === 'RECOVERING' ? 'UNKNOWN' : 'NON_TERMINAL',
1198
+ summary: `Governance Fact: Lease ${boundedText(lease.state)}`,
1199
+ requiresOwnerAction: lease.state === 'RECOVERING',
1200
+ rawEvidenceAvailable: lease.release_evidence_json !== null,
1201
+ });
1202
+ }
1203
+ for (const dispatch of store.listDispatches(kingdomId)) {
1204
+ const refs = [tableSource('dispatch_records', dispatch.dispatch_id)];
1205
+ items.push({
1206
+ id: timelineId('dispatch', dispatch.dispatch_id),
1207
+ kind: 'GOVERNANCE_FACT',
1208
+ occurredAt: safeDate(dispatch.updated_at ?? dispatch.created_at),
1209
+ entityRef: publicEntityRef('task', dispatch.task_id),
1210
+ authoritativeState: stateValue(dispatch.state, 'GOVERNANCE_FACT', refs),
1211
+ sourceRefs: boundedSourceRefs(refs),
1212
+ allowedActions: null,
1213
+ attentionReason: timelineAttention(attentions, publicEntityRef('task', dispatch.task_id)),
1214
+ terminality: dispatch.state === 'TERMINAL' || dispatch.state === 'FAILED'
1215
+ ? 'TERMINAL' : dispatch.state === 'RECOVERING' ? 'UNKNOWN' : 'NON_TERMINAL',
1216
+ summary: `Governance Fact: Dispatch ${boundedText(dispatch.state)}`,
1217
+ requiresOwnerAction: dispatch.state === 'RECOVERING' || !dispatch.terminal_evidence_json,
1218
+ rawEvidenceAvailable: dispatch.receipt_json !== null || dispatch.terminal_evidence_json !== null,
1219
+ });
1220
+ }
1221
+ }
1222
+ for (const item of attentions) {
1223
+ items.push({
1224
+ id: `attention:${item.id}`,
1225
+ kind: 'DERIVED_EXPLANATION',
1226
+ occurredAt: null,
1227
+ entityRef: item.entityRef,
1228
+ authoritativeState: null,
1229
+ sourceRefs: item.sourceRefs,
1230
+ allowedActions: null,
1231
+ attentionReason: item.reason,
1232
+ terminality: 'UNKNOWN',
1233
+ summary: `Derived Explanation: ${boundedText(item.reason.code)} — ${item.summary}`,
1234
+ requiresOwnerAction: item.severity !== 'UNKNOWN',
1235
+ rawEvidenceAvailable: false,
1236
+ });
1237
+ }
1238
+ return retainTimelineEvidence(items);
1239
+ }
1240
+ function ownerActionAvailability() {
1241
+ return OWNER_ONLY_ACTIONS.map(action => {
1242
+ const refs = boundedSourceRefs([ruleSource('OWNER_DIRECT_SLASH_ONLY')]);
1243
+ return {
1244
+ action,
1245
+ lifecycleAllowed: true,
1246
+ executable: false,
1247
+ disabledReason: attentionReason('DIRECT_SLASH_REQUIRED', refs),
1248
+ sourceRefs: refs,
1249
+ };
1250
+ });
1251
+ }
1252
+ function taskAttention(attentions, taskId) {
1253
+ return attentions.find(item => item.entityRef?.type === 'task' && item.entityRef.id === publicReferenceId(taskId))?.reason ?? null;
1254
+ }
1255
+ /** Task Detail 的只读派生 Envelope;保留旧 TaskDetailView 字段,不复刻状态机。 */
1256
+ export function buildTaskProjection(store, kingdomId, task, options = {}) {
1257
+ const refreshedAt = new Date(options.nowMs ?? Date.now()).toISOString();
1258
+ const taskRef = publicEntityRef('task', task.task_id) ?? { type: 'task', id: 'unknown' };
1259
+ const statusRefs = [tableSource('tasks', task.task_id)];
1260
+ const claim = store.latestWorkerResult(task.task_id);
1261
+ const execution = store.latestExecution(task.task_id);
1262
+ const actions = buildActionAvailability(store, task, execution, options.security);
1263
+ const attentions = buildAttention(store, kingdomId);
1264
+ const refs = [...statusRefs];
1265
+ if (claim)
1266
+ refs.push(tableSource('worker_results', claim.result_id));
1267
+ if (execution)
1268
+ refs.push(tableSource('executions', execution.execution_id));
1269
+ const taskAttentionReason = taskAttention(attentions, task.task_id);
1270
+ return projectionEnvelope({
1271
+ revision: store.revision(kingdomId),
1272
+ refreshedAt,
1273
+ entityRef: taskRef,
1274
+ authoritativeState: stateValue(task.status, 'GOVERNANCE_FACT', statusRefs),
1275
+ sourceRefs: refs,
1276
+ allowedActions: actions,
1277
+ attentionReason: taskAttentionReason,
1278
+ data: {
1279
+ taskRef,
1280
+ status: stateValue(task.status, 'GOVERNANCE_FACT', statusRefs),
1281
+ claim: claim ? { outcome: boundedText(claim.outcome) || 'UNKNOWN', sourceRefs: boundedSourceRefs([tableSource('worker_results', claim.result_id)]) } : null,
1282
+ execution: execution ? {
1283
+ state: boundedText(execution.state) || 'UNKNOWN',
1284
+ executionContract: boundedText(execution.execution_contract) || 'UNKNOWN',
1285
+ terminality: executionTerminality(execution.state),
1286
+ sourceRefs: boundedSourceRefs([tableSource('executions', execution.execution_id)]),
1287
+ } : null,
1288
+ actionAvailability: actions,
1289
+ },
1290
+ });
1291
+ }
1292
+ function buildOrganizationProjectionData(store, kingdomId, tasks) {
1293
+ const kingdom = store.getDefaultKingdom();
1294
+ if (!kingdom || kingdom.kingdom_id !== kingdomId) {
1295
+ return {
1296
+ kingdomName: null,
1297
+ bindingCount: 0,
1298
+ territoryCount: 0,
1299
+ roles: [],
1300
+ territories: [],
1301
+ rolesTruncated: false,
1302
+ territoriesTruncated: false,
1303
+ };
1304
+ }
1305
+ // Organization is a live role projection, not a historical binding ledger.
1306
+ // Retired bindings may remain referenced by old Tasks/Territories, but they
1307
+ // must not acquire a territoryRef or enter the console organogram.
1308
+ const bindings = store.listBindings(kingdomId).filter(binding => binding.status === 'ACTIVE');
1309
+ const territoryRows = store.listTerritories(kingdomId);
1310
+ const territoryRefByBinding = new Map();
1311
+ const registerTerritory = (bindingId, territoryId) => {
1312
+ const territoryRef = publicEntityRef('territory', territoryId);
1313
+ if (!territoryRef)
1314
+ return;
1315
+ if (!territoryRefByBinding.has(bindingId)) {
1316
+ territoryRefByBinding.set(bindingId, territoryRef);
1317
+ return;
1318
+ }
1319
+ const previous = territoryRefByBinding.get(bindingId);
1320
+ if (previous === undefined) {
1321
+ territoryRefByBinding.set(bindingId, territoryRef);
1322
+ return;
1323
+ }
1324
+ if (previous === null || previous.id !== territoryRef.id)
1325
+ territoryRefByBinding.set(bindingId, null);
1326
+ };
1327
+ for (const territory of territoryRows) {
1328
+ if (territory.supervisor_binding_id)
1329
+ registerTerritory(territory.supervisor_binding_id, territory.territory_id);
1330
+ }
1331
+ for (const task of tasks) {
1332
+ if (task.assigned_binding_id)
1333
+ registerTerritory(task.assigned_binding_id, task.territory_id);
1334
+ }
1335
+ for (const binding of bindings) {
1336
+ if (binding.role_type !== 'WORKER')
1337
+ continue;
1338
+ const affinity = store.getCurrentAffinityForWorker(kingdomId, binding.binding_id);
1339
+ if (affinity)
1340
+ registerTerritory(binding.binding_id, affinity.territory_id);
1341
+ }
1342
+ const orderedBindings = [...bindings].sort((left, right) => {
1343
+ const statusOrder = Number(right.status === 'ACTIVE') - Number(left.status === 'ACTIVE');
1344
+ return statusOrder
1345
+ || left.role_type.localeCompare(right.role_type)
1346
+ || left.role_name.localeCompare(right.role_name)
1347
+ || left.binding_id.localeCompare(right.binding_id);
1348
+ });
1349
+ const roles = [];
1350
+ for (const binding of orderedBindings) {
1351
+ if (roles.length >= MAX_PROJECTION_ORGANIZATION_ITEMS)
1352
+ break;
1353
+ const bindingRef = publicEntityRef('binding', binding.binding_id);
1354
+ if (!bindingRef)
1355
+ continue;
1356
+ const refs = boundedSourceRefs([tableSource('role_bindings', binding.binding_id)]);
1357
+ roles.push({
1358
+ bindingRef,
1359
+ roleType: boundedText(binding.role_type) || 'UNKNOWN',
1360
+ roleName: boundedText(binding.role_name) || 'UNKNOWN',
1361
+ territoryRef: territoryRefByBinding.get(binding.binding_id) ?? null,
1362
+ status: stateValue(binding.status, 'GOVERNANCE_FACT', refs),
1363
+ sessionBound: binding.session_id !== null,
1364
+ sourceRefs: refs,
1365
+ });
1366
+ }
1367
+ const taskCounts = new Map();
1368
+ for (const task of tasks)
1369
+ taskCounts.set(task.territory_id, (taskCounts.get(task.territory_id) ?? 0) + 1);
1370
+ const territories = [];
1371
+ for (const territory of territoryRows) {
1372
+ if (territories.length >= MAX_PROJECTION_ORGANIZATION_ITEMS)
1373
+ break;
1374
+ const territoryRef = publicEntityRef('territory', territory.territory_id);
1375
+ if (!territoryRef)
1376
+ continue;
1377
+ const refs = boundedSourceRefs([
1378
+ tableSource('territories', territory.territory_id),
1379
+ ...(territory.supervisor_binding_id
1380
+ ? [tableSource('role_bindings', territory.supervisor_binding_id)]
1381
+ : [ruleSource('TERRITORY_SUPERVISOR_MISSING')]),
1382
+ ]);
1383
+ territories.push({
1384
+ territoryRef,
1385
+ name: boundedText(territory.name) || 'UNKNOWN',
1386
+ status: stateValue(territory.status, 'GOVERNANCE_FACT', refs),
1387
+ supervisorBindingRef: publicEntityRef('binding', territory.supervisor_binding_id),
1388
+ taskCount: taskCounts.get(territory.territory_id) ?? 0,
1389
+ sourceRefs: refs,
1390
+ });
1391
+ }
1392
+ return {
1393
+ kingdomName: boundedText(kingdom.name) || 'UNKNOWN',
1394
+ bindingCount: bindings.length,
1395
+ territoryCount: territoryRows.length,
1396
+ roles,
1397
+ territories,
1398
+ rolesTruncated: bindings.length > roles.length,
1399
+ territoriesTruncated: territoryRows.length > territories.length,
1400
+ };
1401
+ }
1402
+ function executionEvidenceTime(execution) {
1403
+ const timestamp = Date.parse(execution.ended_at ?? execution.heartbeat_at ?? execution.started_at);
1404
+ return Number.isFinite(timestamp) ? timestamp : Number.NEGATIVE_INFINITY;
1405
+ }
1406
+ function buildExecutionProjectionData(store, tasks, attentions, security) {
1407
+ const candidates = [];
1408
+ for (const task of tasks) {
1409
+ const executions = store.listExecutions(task.task_id);
1410
+ for (let index = 0; index < executions.length; index += 1) {
1411
+ candidates.push({ task, execution: executions[index], latest: index === executions.length - 1 });
1412
+ }
1413
+ }
1414
+ candidates.sort((left, right) => executionEvidenceTime(right.execution) - executionEvidenceTime(left.execution)
1415
+ || right.execution.attempt_no - left.execution.attempt_no
1416
+ || left.execution.execution_id.localeCompare(right.execution.execution_id));
1417
+ const items = [];
1418
+ for (const candidate of candidates) {
1419
+ if (items.length >= MAX_PROJECTION_EXECUTION_SUMMARIES)
1420
+ break;
1421
+ const { task, execution, latest } = candidate;
1422
+ const executionRef = publicEntityRef('execution', execution.execution_id);
1423
+ const taskRef = publicEntityRef('task', task.task_id);
1424
+ if (!executionRef || !taskRef)
1425
+ continue;
1426
+ const refs = boundedSourceRefs([
1427
+ tableSource('executions', execution.execution_id),
1428
+ tableSource('tasks', task.task_id),
1429
+ ...(execution.worker_binding_id ? [tableSource('role_bindings', execution.worker_binding_id)] : []),
1430
+ ...(execution.lease_id ? [tableSource('execution_leases', execution.lease_id)] : []),
1431
+ ...(execution.capability_decision_id
1432
+ ? [tableSource('capability_decisions', execution.capability_decision_id)]
1433
+ : []),
1434
+ ]);
1435
+ const actionAvailability = latest
1436
+ ? buildActionAvailability(store, task, execution, security)
1437
+ .filter(action => action.action.startsWith('execution:'))
1438
+ : [];
1439
+ const state = boundedText(execution.state) || 'UNKNOWN';
1440
+ items.push({
1441
+ executionId: executionRef.id,
1442
+ taskId: taskRef.id,
1443
+ executionRef,
1444
+ taskRef,
1445
+ workerBindingRef: publicEntityRef('binding', execution.worker_binding_id),
1446
+ attemptNo: Number.isSafeInteger(execution.attempt_no) && execution.attempt_no >= 0
1447
+ ? execution.attempt_no
1448
+ : 0,
1449
+ state,
1450
+ authoritativeState: stateValue(state, 'RUNTIME_OBSERVATION', [tableSource('executions', execution.execution_id)]),
1451
+ executionContract: boundedText(execution.execution_contract) || 'UNKNOWN',
1452
+ terminality: executionTerminality(execution.state),
1453
+ pausePending: execution.pause_requested_at !== null && execution.state === 'RUNNING',
1454
+ startedAt: safeDate(execution.started_at),
1455
+ endedAt: safeDate(execution.ended_at),
1456
+ actionAvailability,
1457
+ attentionReason: taskAttention(attentions, task.task_id),
1458
+ sourceRefs: refs,
1459
+ });
1460
+ }
1461
+ return {
1462
+ totalExecutionCount: candidates.length,
1463
+ items,
1464
+ truncated: candidates.length > items.length,
1465
+ };
1466
+ }
1467
+ /** Keep runtime references and prompt text outside the public cost projection. */
1468
+ function buildWorkbenchCost(store, kingdomId, present, runtime) {
1469
+ const number = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
1470
+ const rows = present ? readLatestPromptCosts(store, kingdomId) : [];
1471
+ const parts = (value) => Array.isArray(value)
1472
+ ? value.slice(0, 24).map(item => ({ name: boundedText(typeof item?.name === 'string' ? item.name : 'UNKNOWN'), bytes: number(item?.bytes) })) : [];
1473
+ return {
1474
+ additionalRoles: present ? readAdditionalRoleCost(store, kingdomId) : null,
1475
+ budget: present ? readBudgetView(store, kingdomId) : null,
1476
+ prompts: { totalCount: rows.length, truncated: rows.length > 40, items: rows.slice(0, 40).map(row => ({
1477
+ bindingId: publicReferenceId(row.bindingId), roleType: boundedText(row.roleType),
1478
+ toolsBefore: number(row.toolsBefore), toolsAfter: number(row.toolsAfter), toolsBytesBefore: number(row.toolsBytesBefore), toolsBytesAfter: number(row.toolsBytesAfter),
1479
+ sections: parts(row.sections), contexts: parts(row.contexts),
1480
+ partsTruncated: (Array.isArray(row.sections) && row.sections.length > 24) || (Array.isArray(row.contexts) && row.contexts.length > 24),
1481
+ historyBytes: null, tokenEstimate: null, mode: row.mode === 'off' || row.mode === 'pilot' ? row.mode : 'UNKNOWN',
1482
+ reasonCode: typeof row.reasonCode === 'string' ? boundedText(row.reasonCode) : null,
1483
+ })) },
1484
+ runtime: { toolDisclosureMode: runtime?.toolDisclosureMode === 'off' || runtime?.toolDisclosureMode === 'pilot' ? runtime.toolDisclosureMode : 'UNKNOWN',
1485
+ observerAvailable: typeof runtime?.observerAvailable === 'boolean' ? runtime.observerAvailable : null },
1486
+ };
1487
+ }
1488
+ /** Apply the bound after exact task selection, so unrelated activity cannot erase task history. */
1489
+ function readTaskEvents(store, kingdomId, taskId, limit) {
1490
+ return store.db.prepare('SELECT * FROM events WHERE kingdom_id = ? AND target_type = ? AND target_id = ? ORDER BY seq DESC LIMIT ?')
1491
+ .all(kingdomId, 'task', taskId, limit);
1492
+ }
1493
+ function readTaskReviewEvents(store, kingdomId, taskId, limit) {
1494
+ return store.db.prepare("SELECT * FROM events WHERE kingdom_id = ? AND target_type = 'task' AND target_id = ? AND actor_role = 'SUPERVISOR' AND event_type IN ('TASK_ACCEPTED', 'TASK_REWORK_REQUESTED', 'TASK_FAILED', 'TASK_HANDED_OFF') ORDER BY seq DESC LIMIT ?")
1495
+ .all(kingdomId, taskId, limit);
1496
+ }
1497
+ /** Canonical reads remain here; the workbench builder only receives public projection values. */
1498
+ function buildWorkbenchCollaboration(store, kingdomId, present) {
1499
+ const publicId = (id) => publicReferenceId(id) ?? 'redacted';
1500
+ const plans = present ? listCollaborationPlans(store, kingdomId) : [];
1501
+ const resources = present && store.isSchemaV4 ? readWorkspaceReservations(store, kingdomId) : [];
1502
+ const readiness = (value) => value ? {
1503
+ ready: value.ready, reasonCode: value.reasonCode, blockingTaskIds: value.blockingTaskIds.map(publicId),
1504
+ acceptedResults: value.handoffs.map(item => ({ taskId: publicId(item.taskId), attemptNo: item.attemptNo,
1505
+ resultId: publicId(item.resultId), resultDigest: item.resultDigest, acceptEventId: publicId(item.acceptEventId) })),
1506
+ } : null;
1507
+ return {
1508
+ pendingAdoptionCount: plans.filter(plan => plan.state === 'PROPOSED').length,
1509
+ plans: { totalCount: plans.length, truncated: plans.length > 40, items: plans.slice(0, 40).map(plan => ({
1510
+ planId: publicId(plan.planId), parentTaskId: publicId(plan.parentTaskId), title: boundedText(store.getTask(plan.parentTaskId)?.title),
1511
+ version: plan.version, digest: plan.digest, state: plan.state, mode: plan.mode, reason: boundedText(plan.reason),
1512
+ integratorBindingId: publicId(plan.integratorBindingId), integratorName: boundedText(store.getBindingById(plan.integratorBindingId)?.role_name),
1513
+ parentStatus: store.getTask(plan.parentTaskId)?.status ?? 'UNKNOWN', budgetTokens: plan.budgetTokens, reserveTokens: plan.reserveTokens,
1514
+ adoptedAt: plan.adoptedAt, integration: readiness(readCollaborationReadiness(store, kingdomId, plan.parentTaskId)),
1515
+ budget: readPlanBudgetView(store, kingdomId, plan.planId),
1516
+ items: plan.items.map(item => ({ key: boundedText(item.key), taskId: publicId(item.taskId), title: boundedText(item.title),
1517
+ description: boundedText(item.description), acceptanceCriteria: boundedText(item.acceptanceCriteria),
1518
+ territoryId: publicId(item.territoryId), territoryName: boundedText(store.getTerritoryById(item.territoryId)?.name),
1519
+ workerBindingId: publicId(item.workerBindingId), workerName: boundedText(store.getBindingById(item.workerBindingId)?.role_name),
1520
+ access: item.access, dependsOn: item.dependsOn.map(boundedText), expectedArtifact: boundedText(item.expectedArtifact),
1521
+ status: store.getTask(item.taskId)?.status ?? 'NOT_CREATED', readiness: readiness(readCollaborationReadiness(store, kingdomId, item.taskId)) })),
1522
+ })) },
1523
+ resources: { totalCount: resources.length, truncated: resources.length > 80, items: resources.slice(0, 80).map(item => ({
1524
+ taskId: publicId(item.taskId), attemptNo: item.attemptNo, access: item.access, state: boundedText(item.state), recovery: item.recovery,
1525
+ })) },
1526
+ };
1527
+ }
1528
+ function buildWorkbenchData(store, kingdomId, tasks, security, costRuntime) {
1529
+ const kingdom = store.getDefaultKingdom();
1530
+ const present = kingdom?.kingdom_id === kingdomId;
1531
+ const bindings = present ? store.listBindings(kingdomId) : [];
1532
+ const territories = present ? store.listTerritories(kingdomId) : [];
1533
+ const publicId = (id) => publicReferenceId(id) ?? 'redacted';
1534
+ const taskInputs = tasks.map(task => {
1535
+ const view = toTaskView(store, task);
1536
+ const claim = store.latestWorkerResult(task.task_id);
1537
+ const execution = store.latestExecution(task.task_id);
1538
+ const review = readTaskReviewEvents(store, kingdomId, task.task_id, 1)[0];
1539
+ return {
1540
+ task: {
1541
+ taskId: publicId(task.task_id), territoryId: publicId(task.territory_id), title: boundedText(task.title),
1542
+ status: boundedText(task.status), assignedBindingId: publicReferenceId(task.assigned_binding_id), updatedAt: safeDate(task.updated_at) ?? '',
1543
+ latestClaim: view.latestClaim ? { ...view.latestClaim, resultId: publicId(view.latestClaim.resultId),
1544
+ workerBindingId: publicReferenceId(view.latestClaim.workerBindingId), sessionId: null } : null,
1545
+ latestExecution: view.latestExecution ? { ...view.latestExecution, executionId: publicId(view.latestExecution.executionId),
1546
+ taskId: publicId(task.task_id), workerBindingId: publicReferenceId(view.latestExecution.workerBindingId), sessionId: null,
1547
+ leaseId: publicReferenceId(view.latestExecution.leaseId), capabilityDecisionId: publicReferenceId(view.latestExecution.capabilityDecisionId) } : null,
1548
+ },
1549
+ actionAvailability: buildActionAvailability(store, task, execution, security),
1550
+ latestReview: review ? toSupervisorDecisionView(review) : null,
1551
+ claimExecutionMismatch: Boolean(claim && execution && claimExecutionMismatch(claim, execution)),
1552
+ };
1553
+ });
1554
+ const governance = present ? buildGovernance(store, kingdomId) : emptyGovernance();
1555
+ // Only public IDs, reason codes and typed token observations can be consumed by the workbench.
1556
+ governance.decisions = governance.decisions.map(decision => ({ ...decision, decisionId: publicId(decision.decisionId), taskId: publicId(decision.taskId), reasonCode: boundedText(decision.reasonCode) || null }));
1557
+ governance.dispatches = governance.dispatches.map(dispatch => ({ ...dispatch, dispatchId: publicId(dispatch.dispatchId), taskId: publicId(dispatch.taskId),
1558
+ executionId: publicId(dispatch.executionId), leaseId: publicId(dispatch.leaseId), runtimeDispatchRef: null, runtimeExecutionRef: null }));
1559
+ governance.leases = governance.leases.map(lease => ({ ...lease, leaseId: publicId(lease.leaseId), taskId: publicId(lease.taskId) }));
1560
+ return buildPersonalWorkbench({
1561
+ kingdomPresent: present,
1562
+ bindings: bindings.map(binding => ({ bindingId: publicId(binding.binding_id), roleType: boundedText(binding.role_type), roleName: boundedText(binding.role_name),
1563
+ status: boundedText(binding.status), sessionBound: binding.session_id !== null })),
1564
+ territories: territories.map(territory => ({ territoryId: publicId(territory.territory_id), name: boundedText(territory.name), status: boundedText(territory.status),
1565
+ supervisorBindingId: publicReferenceId(territory.supervisor_binding_id) })),
1566
+ tasks: taskInputs,
1567
+ executions: tasks.flatMap(task => store.listExecutions(task.task_id).map(execution => ({ executionId: publicId(execution.execution_id), taskId: publicId(task.task_id),
1568
+ workerBindingId: publicReferenceId(execution.worker_binding_id), state: boundedText(execution.state) }))),
1569
+ governance,
1570
+ cost: buildWorkbenchCost(store, kingdomId, present, costRuntime),
1571
+ collaboration: buildWorkbenchCollaboration(store, kingdomId, present),
1572
+ });
1573
+ }
1574
+ /** S1 顶层只读 Projection;没有 kingdom 时显式返回 UNKNOWN/空数据。 */
1575
+ export function buildReadonlySnapshotProjection(store, kingdomId, options = {}) {
1576
+ const nowMs = options.nowMs ?? Date.now();
1577
+ const refreshedAt = new Date(nowMs).toISOString();
1578
+ const kingdom = store.getDefaultKingdom();
1579
+ const actualKingdomId = kingdom?.kingdom_id ?? kingdomId ?? '';
1580
+ const revision = actualKingdomId ? store.revision(actualKingdomId) : 0;
1581
+ const kingdomRef = publicEntityRef('kingdom', actualKingdomId);
1582
+ const sourceRefs = kingdomRef ? [tableSource('kingdoms', actualKingdomId)] : [ruleSource('KINGDOM_PRESENT')];
1583
+ const attentions = actualKingdomId
1584
+ ? buildAttention(store, actualKingdomId)
1585
+ : [attentionItem('kingdom:missing', 'UNKNOWN', null, 'CONFIGURATION_INCOMPLETE', 'Kingdom is not initialized', [ruleSource('KINGDOM_PRESENT')])];
1586
+ const timeline = actualKingdomId
1587
+ ? buildTimeline(store, actualKingdomId, options)
1588
+ : [];
1589
+ const tasks = actualKingdomId ? store.listTasks(actualKingdomId) : [];
1590
+ const activeExecutionCount = actualKingdomId ? store.listLiveExecutions(actualKingdomId).length : 0;
1591
+ const statusCounts = {};
1592
+ for (const task of tasks)
1593
+ statusCounts[task.status] = (statusCounts[task.status] ?? 0) + 1;
1594
+ const organizationData = buildOrganizationProjectionData(store, actualKingdomId, tasks);
1595
+ const executionData = buildExecutionProjectionData(store, tasks, attentions, options.security);
1596
+ const health = attentions.some(item => item.severity === 'CRITICAL')
1597
+ ? 'CRITICAL'
1598
+ : attentions.some(item => item.severity === 'ATTENTION')
1599
+ ? 'ATTENTION'
1600
+ : kingdom ? 'OK' : 'UNKNOWN';
1601
+ const topAttention = attentions[0]?.reason ?? null;
1602
+ const authoritative = kingdom
1603
+ ? stateValue('PRESENT', 'GOVERNANCE_FACT', sourceRefs)
1604
+ : null;
1605
+ return {
1606
+ overview: projectionEnvelope({
1607
+ revision,
1608
+ refreshedAt,
1609
+ entityRef: kingdomRef,
1610
+ authoritativeState: authoritative,
1611
+ sourceRefs,
1612
+ attentionReason: topAttention,
1613
+ data: { health, taskCount: tasks.length, activeExecutionCount, statusCounts, ownerActions: ownerActionAvailability() },
1614
+ }),
1615
+ organization: projectionEnvelope({
1616
+ revision,
1617
+ refreshedAt,
1618
+ entityRef: kingdomRef,
1619
+ authoritativeState: authoritative,
1620
+ sourceRefs: [...sourceRefs, ruleSource('ORGANIZATION_SUMMARY_DERIVED')],
1621
+ attentionReason: topAttention,
1622
+ data: organizationData,
1623
+ }),
1624
+ executions: projectionEnvelope({
1625
+ revision,
1626
+ refreshedAt,
1627
+ entityRef: kingdomRef,
1628
+ authoritativeState: null,
1629
+ sourceRefs: [...sourceRefs, ruleSource('EXECUTION_SUMMARY_DERIVED')],
1630
+ attentionReason: topAttention,
1631
+ data: executionData,
1632
+ }),
1633
+ timeline: projectionEnvelope({
1634
+ revision,
1635
+ refreshedAt,
1636
+ entityRef: kingdomRef,
1637
+ authoritativeState: null,
1638
+ sourceRefs: [...sourceRefs, ruleSource('TIMELINE_RECONSTRUCTED')],
1639
+ data: timeline,
1640
+ }),
1641
+ attention: projectionEnvelope({
1642
+ revision,
1643
+ refreshedAt,
1644
+ entityRef: kingdomRef,
1645
+ authoritativeState: null,
1646
+ sourceRefs: [...sourceRefs, ruleSource('ATTENTION_DERIVED')],
1647
+ attentionReason: topAttention,
1648
+ data: attentions,
1649
+ }),
1650
+ workbench: projectionEnvelope({
1651
+ revision,
1652
+ refreshedAt,
1653
+ entityRef: kingdomRef,
1654
+ authoritativeState: null,
1655
+ sourceRefs: [...sourceRefs, ruleSource('PERSONAL_WORKBENCH_DERIVED')],
1656
+ data: buildWorkbenchData(store, actualKingdomId, tasks, options.security, options.costRuntime),
1657
+ }),
1658
+ };
1659
+ }
388
1660
  export function buildSnapshot(store, options) {
389
1661
  const kingdom = store.getDefaultKingdom();
390
1662
  const nowMs = options.nowMs ?? Date.now();
@@ -403,10 +1675,17 @@ export function buildSnapshot(store, options) {
403
1675
  stage: [],
404
1676
  recentEvents: [],
405
1677
  governance: emptyGovernance(),
1678
+ projection: buildReadonlySnapshotProjection(store, null, {
1679
+ eventLimit: options.eventLimit,
1680
+ nowMs,
1681
+ security: options.security,
1682
+ costRuntime: options.costRuntime,
1683
+ }),
406
1684
  };
407
1685
  }
408
1686
  const kingdomId = kingdom.kingdom_id;
409
1687
  const bindings = store.listBindings(kingdomId);
1688
+ const territories = store.listTerritories(kingdomId);
410
1689
  const tasks = store.listTasks(kingdomId);
411
1690
  const events = store.listEvents(kingdomId, options.eventLimit ?? 50);
412
1691
  const liveExecutions = store.listLiveExecutions(kingdomId);
@@ -423,11 +1702,12 @@ export function buildSnapshot(store, options) {
423
1702
  },
424
1703
  auth: options.auth,
425
1704
  bindings: bindings.map(toBindingView),
426
- territories: store.listTerritories(kingdomId).map(toTerritoryView),
1705
+ territories: territories.map(toTerritoryView),
427
1706
  tasks: tasks.map(t => toTaskView(store, t)),
428
1707
  liveExecutions: liveExecutions.map(toExecutionView),
429
1708
  stage: projectStage({
430
1709
  bindings,
1710
+ territories,
431
1711
  tasks,
432
1712
  executions: liveExecutions,
433
1713
  events,
@@ -436,9 +1716,15 @@ export function buildSnapshot(store, options) {
436
1716
  }),
437
1717
  recentEvents: events.map(toEventView),
438
1718
  governance: buildGovernance(store, kingdomId),
1719
+ projection: buildReadonlySnapshotProjection(store, kingdomId, {
1720
+ eventLimit: options.eventLimit,
1721
+ nowMs,
1722
+ security: options.security,
1723
+ costRuntime: options.costRuntime,
1724
+ }),
439
1725
  };
440
1726
  }
441
- export function buildTaskDetail(store, kingdomId, taskId) {
1727
+ export function buildTaskDetail(store, kingdomId, taskId, options = {}) {
442
1728
  const task = store.getTask(taskId);
443
1729
  if (!task)
444
1730
  return null;
@@ -446,25 +1732,12 @@ export function buildTaskDetail(store, kingdomId, taskId) {
446
1732
  if (!territory || territory.kingdom_id !== kingdomId)
447
1733
  return null;
448
1734
  const binding = task.assigned_binding_id ? store.getBindingById(task.assigned_binding_id) : null;
1735
+ const assignments = store.listTaskAssignments(taskId);
449
1736
  const executions = store.listExecutions(taskId);
450
- const related = store
451
- .listEvents(kingdomId, 500)
452
- .filter(e => e.target_id === taskId)
453
- .sort((a, b) => a.seq - b.seq);
454
- const reviews = related
455
- .filter(e => ['TASK_ACCEPTED', 'TASK_REWORK_REQUESTED', 'TASK_FAILED'].includes(e.event_type))
456
- .map((e) => {
457
- const payload = parseJson(e.payload_json);
458
- return {
459
- seq: e.seq,
460
- decision: typeof payload.decision === 'string' ? payload.decision : e.event_type,
461
- reason: typeof payload.reason === 'string' ? payload.reason : null,
462
- reviewerBindingId: typeof payload.reviewer_binding_id === 'string' ? payload.reviewer_binding_id : null,
463
- reviewedAttemptNo: typeof payload.reviewed_attempt_no === 'number' ? payload.reviewed_attempt_no : null,
464
- claimedOutcome: typeof payload.claimed_outcome === 'string' ? payload.claimed_outcome : null,
465
- createdAt: e.created_at,
466
- };
467
- });
1737
+ const taskEvents = readTaskEvents(store, kingdomId, taskId, 501);
1738
+ const related = taskEvents.slice(0, 500).sort((a, b) => a.seq - b.seq);
1739
+ const reviewEvents = readTaskReviewEvents(store, kingdomId, taskId, 501);
1740
+ const reviews = reviewEvents.slice(0, 500).sort((a, b) => a.seq - b.seq).map(toSupervisorDecisionView);
468
1741
  const view = toTaskView(store, task);
469
1742
  return {
470
1743
  schemaVersion: GUI_SCHEMA_VERSION,
@@ -472,12 +1745,18 @@ export function buildTaskDetail(store, kingdomId, taskId) {
472
1745
  task: view,
473
1746
  territory: toTerritoryView(territory),
474
1747
  assignedBinding: binding ? toBindingView(binding) : null,
1748
+ assignments: assignments.map(toTaskAssignmentHistoryView),
475
1749
  claims: store.listWorkerResults(taskId).map(toClaimView),
476
1750
  executions: executions.map(toExecutionView),
477
1751
  reviews,
1752
+ reviewsTruncated: reviewEvents.length > reviews.length,
478
1753
  relatedEvents: related.map(toEventView),
1754
+ relatedEventsTruncated: taskEvents.length > related.length,
1755
+ humanAcceptance: 'NOT_RECORDED',
479
1756
  allowedActions: view.allowedActions,
480
1757
  governance: buildTaskGovernance(store, taskId),
1758
+ additionalRoleCost: readAdditionalRoleCost(store, kingdomId, taskId),
1759
+ projection: buildTaskProjection(store, kingdomId, task, options),
481
1760
  };
482
1761
  }
483
1762
  //# sourceMappingURL=snapshot.js.map