@michelj/context-guard 0.4.4 → 0.6.1

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 (101) hide show
  1. package/Coordinator.md +88 -0
  2. package/Executor.md +53 -0
  3. package/README.md +72 -102
  4. package/README.zh-CN.md +72 -102
  5. package/SKILL.md +26 -33
  6. package/THIRD_PARTY_NOTICES.md +47 -0
  7. package/Tester.md +53 -0
  8. package/bin/build-runtime.mjs +96 -0
  9. package/bin/context-guard-skill.js +287 -69
  10. package/bin/postinstall.js +1 -1
  11. package/hooks.json +80 -4
  12. package/licenses/JSONParse-MIT.txt +24 -0
  13. package/licenses/Marked-MIT.txt +44 -0
  14. package/licenses/Portless-Apache-2.0.txt +201 -0
  15. package/package.json +31 -5
  16. package/prototype/LICENSES/Marked-MIT.txt +44 -0
  17. package/prototype/LICENSES/Ready-redistribution.txt +14 -0
  18. package/prototype/attachments.mjs +75 -0
  19. package/prototype/coordinator-markdown.mjs +283 -0
  20. package/prototype/coordinator-working-blot.mjs +124 -0
  21. package/prototype/vendor/marked.mjs +2189 -0
  22. package/prototype/workbench-app.js +5197 -0
  23. package/prototype/workbench-data.js +33 -0
  24. package/prototype/workbench-sync.mjs +898 -0
  25. package/prototype/workbench.css +1050 -0
  26. package/prototype/workbench.html +139 -4861
  27. package/prototype/working-blot-atlas.png +0 -0
  28. package/references/agent-handoff.md +40 -0
  29. package/references/claude-runtime.md +120 -0
  30. package/references/cloud-sync-interface.md +66 -0
  31. package/references/design-current.md +14 -0
  32. package/references/map-mount.md +41 -0
  33. package/references/map-read.md +50 -0
  34. package/references/memory-definition.md +120 -0
  35. package/references/memory-filesystem-v2/Bug.en.md +162 -0
  36. package/references/memory-filesystem-v2/Bug.md +162 -0
  37. package/references/memory-filesystem-v2/Bug_Coordinater.md +8 -0
  38. package/references/memory-filesystem-v2/Bug_Executor.md +8 -0
  39. package/references/memory-filesystem-v2/Bug_Tester.md +7 -0
  40. package/references/memory-filesystem-v2/Idea.en.md +36 -0
  41. package/references/memory-filesystem-v2/Idea.md +36 -0
  42. package/references/memory-filesystem-v2/Node_Module_Index.en.md +88 -0
  43. package/references/memory-filesystem-v2/Node_Module_Index.md +88 -0
  44. package/references/memory-filesystem-v2/README.md +60 -0
  45. package/references/memory-filesystem-v2/Todo.en.md +137 -0
  46. package/references/memory-filesystem-v2/Todo.md +137 -0
  47. package/references/memory-filesystem-v2/Todo_Coordinater.md +7 -0
  48. package/references/memory-filesystem-v2/Todo_Executor.md +7 -0
  49. package/references/memory-filesystem-v2/Todo_Tester.md +7 -0
  50. package/references/named-workbench.md +124 -0
  51. package/references/plan-review.md +12 -0
  52. package/references/server-memory.md +276 -0
  53. package/references/test-check.md +7 -0
  54. package/references/user-reply.md +38 -0
  55. package/references/workbench-interface.md +531 -0
  56. package/roles.md +13 -0
  57. package/scripts/context_guard.py +1163 -321
  58. package/scripts/context_guard_hook.py +1864 -63
  59. package/scripts/map_owns.py +68 -138
  60. package/scripts/shared/LICENSES/JSONParse-MIT.txt +24 -0
  61. package/scripts/shared/filesystem-v2.mjs +430 -0
  62. package/scripts/shared/io.mjs +117 -0
  63. package/scripts/shared/map-model.mjs +506 -0
  64. package/scripts/shared/memory-schema.mjs +13 -0
  65. package/scripts/shared/protocol-blobs.mjs +112 -0
  66. package/scripts/shared/protocol-map.mjs +146 -0
  67. package/scripts/shared/protocol-snapshots.mjs +84 -0
  68. package/scripts/shared/protocol-store.mjs +624 -0
  69. package/scripts/shared/protocol-workflow.mjs +226 -0
  70. package/scripts/shared/protocol.mjs +125 -0
  71. package/scripts/shared/vendor/jsonparse.cjs +413 -0
  72. package/scripts/workbench/access.mjs +496 -0
  73. package/scripts/workbench/attachments.mjs +92 -0
  74. package/scripts/workbench/browser-login.mjs +78 -0
  75. package/scripts/workbench/claude-runtime.mjs +372 -0
  76. package/scripts/workbench/cli.mjs +980 -0
  77. package/scripts/workbench/device-heartbeat.mjs +72 -0
  78. package/scripts/workbench/hook-status.mjs +38 -0
  79. package/scripts/workbench/inbox.mjs +155 -0
  80. package/scripts/workbench/journal.mjs +56 -0
  81. package/scripts/workbench/memory-merge.mjs +65 -0
  82. package/scripts/workbench/memory.mjs +252 -0
  83. package/scripts/workbench/named-proxy.mjs +108 -0
  84. package/scripts/workbench/named.mjs +152 -0
  85. package/scripts/workbench/portless-routes.mjs +51 -0
  86. package/scripts/workbench/project.mjs +327 -0
  87. package/scripts/workbench/projections.mjs +68 -0
  88. package/scripts/workbench/protocol-client.mjs +165 -0
  89. package/scripts/workbench/protocol-delivery.mjs +133 -0
  90. package/scripts/workbench/protocol-device.mjs +316 -0
  91. package/scripts/workbench/protocol-events.mjs +53 -0
  92. package/scripts/workbench/protocol-repository.mjs +58 -0
  93. package/scripts/workbench/reconcile.mjs +244 -0
  94. package/scripts/workbench/registry.mjs +111 -0
  95. package/scripts/workbench/runtime.mjs +54 -0
  96. package/scripts/workbench/server.mjs +1171 -0
  97. package/scripts/workbench/store.mjs +243 -0
  98. package/scripts/workbench/sync-coordinator.mjs +518 -0
  99. package/scripts/workbench/sync.mjs +86 -0
  100. package/references/bug-record-template.md +0 -37
  101. package/references/context-template.md +0 -19
@@ -0,0 +1,624 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { EventEmitter } from 'node:events';
5
+ import { atomicWrite, encode, hash, readJSON, withFileLock } from './io.mjs';
6
+ import { canonical, fail, validateMessage, MAX_MESSAGE_BYTES } from './protocol.mjs';
7
+ import { reduceWorkflow, scopedObjectKey, workflowTypes } from './protocol-workflow.mjs';
8
+
9
+ const fresh = () => ({ format: 1, bindings: {}, receipts: {}, queues: {}, objects: {}, tasks: {} });
10
+ const observations = new Set(['sync.heartbeat', 'sync.read', 'object.read', 'workbench.read', 'blob.get']);
11
+ const key = value => hash(canonical(value));
12
+ const queueKey = (principal, session) => key([principal.repositoryId, session.id, session.generation]);
13
+ const principalKey = p => [p.repositoryId, p.deviceId, p.agentId];
14
+ const requireIdentity = p => {
15
+ if (!p || ['repositoryId', 'deviceId', 'agentId'].some(k => typeof p[k] !== 'string' || !p[k])) fail('UNAUTHORIZED', 'Authenticated identity required');
16
+ };
17
+ const bindingKey = (p, id) => key([p.repositoryId, id]);
18
+ export function hasCiReceiver(state, principal, session, config = {}) {
19
+ const executor = state.bindings[bindingKey(principal, session.id)];
20
+ if (!executor || executor.generation !== session.generation) return false;
21
+ const creation = Object.values(state.sessionCreations || {}).find(item =>
22
+ item.repositoryId === principal.repositoryId && item.deviceId === executor.deviceId &&
23
+ item.result?.sessionId === session.id && item.result.state === 'registered' && item.result.generation === session.generation);
24
+ return Object.entries(config.ciReceivers || {}).filter(([id, receiver]) => {
25
+ if (receiver.executorSessionId !== session.id && !(config.sessionTemplates?.includes(receiver.executorSessionId) &&
26
+ creation?.result.templateSessionId === receiver.executorSessionId)) return false;
27
+ const ci = state.bindings[bindingKey(principal, id)];
28
+ return ci && ci.deviceId === executor.deviceId && ci.worktreeId === receiver.worktreeId && ci.worktreeId !== executor.worktreeId;
29
+ }).length === 1;
30
+ }
31
+ const requireBinding = (state, p, session) => {
32
+ const binding = state.bindings[bindingKey(p, session.id)];
33
+ const delegated = binding && ['coordinator', 'ci'].includes(p.role) && p.bindings?.[session.id] === binding.worktreeId;
34
+ if (!binding || !delegated && p.role !== 'human' && (binding.deviceId !== p.deviceId || (p.role !== 'device' && binding.agentId !== p.agentId))) fail('FORBIDDEN', 'Session is not assigned to this identity');
35
+ if (binding.generation !== session.generation) fail('STALE_SESSION', 'Session binding changed', { currentGeneration: binding.generation });
36
+ return binding;
37
+ };
38
+ const queueFor = (state, p, session) => state.queues[queueKey(p, session)] ||= { latestSeq: 0, ackedSeq: 0, items: [], outcomes: {} };
39
+ const consumerFor = (queue, p, create = false) => {
40
+ const id = key(principalKey(p));
41
+ if (create) { queue.consumers ||= {}; queue.consumers[id] ||= { ackedSeq: 0, outcomes: {} }; }
42
+ return queue.consumers?.[id] || { ackedSeq: 0, outcomes: {} };
43
+ };
44
+
45
+ // One transaction persists business state, emitted messages and the retry receipt.
46
+ // Reducers must only mutate this transaction, never perform external side effects.
47
+ export class ProtocolStore extends EventEmitter {
48
+ constructor(directory, { beforeCommit = async () => {} } = {}) { super(); this.file = path.join(directory, 'protocol-v2.json'); this.beforeCommit = beforeCommit; }
49
+ async immutableState() {
50
+ const stamp = async () => {
51
+ try { const value = await fs.stat(this.file, { bigint: true }); return `${value.ino}:${value.size}:${value.mtimeNs}:${value.ctimeNs}`; }
52
+ catch (error) { if (error.code === 'ENOENT') return ''; throw error; }
53
+ };
54
+ for (let attempt = 0; attempt < 3; attempt++) {
55
+ const before = await stamp();
56
+ if (this.cached?.stamp === before) return this.cached.state;
57
+ const state = await readJSON(this.file, fresh());
58
+ if (state.format !== 1) fail('UNAVAILABLE', 'Unsupported stored protocol version');
59
+ if (before !== await stamp()) continue;
60
+ const pending = [state];
61
+ while (pending.length) {
62
+ const value = pending.pop();
63
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) continue;
64
+ for (const child of Object.values(value)) pending.push(child);
65
+ Object.freeze(value);
66
+ }
67
+ this.cached = { stamp: before, state };
68
+ return state;
69
+ }
70
+ fail('UNAVAILABLE', 'Protocol storage changed during the read');
71
+ }
72
+ async transaction(action, { readOnly = false } = {}) {
73
+ if (readOnly) return structuredClone(await action(await this.immutableState()));
74
+ const perform = async () => {
75
+ const state = await readJSON(this.file, fresh());
76
+ if (state.format !== 1) fail('UNAVAILABLE', 'Unsupported stored protocol version');
77
+ const before = encode(state);
78
+ const result = await action(state);
79
+ const after = encode(state);
80
+ if (after !== before) {
81
+ await this.beforeCommit(); await atomicWrite(this.file, after);
82
+ this.cached = null;
83
+ }
84
+ return structuredClone(result);
85
+ };
86
+ return withFileLock(`${this.file}.lock`, perform);
87
+ }
88
+ async execute(principal, input, reduce, authorize = () => {}) {
89
+ requireIdentity(principal); validateMessage(input);
90
+ if (input.type.startsWith('auth.')) fail('FORBIDDEN', 'Credentials must not enter the message journal');
91
+ const reply = await this.transaction(async state => {
92
+ if (input.session) requireBinding(state, principal, input.session);
93
+ await authorize(state, principal, input);
94
+ const receiptKey = key([...principalKey(principal), input.id]);
95
+ const fingerprint = hash(canonical(input));
96
+ const receipt = observations.has(input.type) ? null : state.receipts[receiptKey];
97
+ if (receipt) {
98
+ if (receipt.fingerprint !== fingerprint) fail('ID_REUSED', 'Request ID already has different content');
99
+ return receipt.reply;
100
+ }
101
+ const emit = message => {
102
+ validateMessage(message);
103
+ if (!message.session || !state.bindings[bindingKey(principal, message.session.id)]) fail('NOT_FOUND', 'Notification Session does not exist');
104
+ const binding = state.bindings[bindingKey(principal, message.session.id)];
105
+ if (binding.generation !== message.session.generation) fail('STALE_SESSION', 'Notification targets an old binding');
106
+ const queue = queueFor(state, principal, message.session);
107
+ const previous = queue.items.find(i => i.message.id === message.id);
108
+ if (previous) {
109
+ if (canonical(previous.message) !== canonical(message)) fail('ID_REUSED', 'Notification ID already has different content');
110
+ return previous.seq;
111
+ }
112
+ // Reserve space for the enclosing read response: a legal message must fit a page.
113
+ if (Buffer.byteLength(canonical(message)) > MAX_MESSAGE_BYTES - 1024) fail('TOO_LARGE', 'Notification exceeds the page budget; use an object reference');
114
+ const seq = ++queue.latestSeq;
115
+ queue.items.push({ seq, message: structuredClone(message) });
116
+ return seq;
117
+ };
118
+ const data = await reduce(state, principal, input, emit);
119
+ const reply = { id: input.id, ok: true, data };
120
+ if (Buffer.byteLength(JSON.stringify(reply)) > MAX_MESSAGE_BYTES) fail('TOO_LARGE', 'Reply exceeds the page budget');
121
+ if (!observations.has(input.type)) state.receipts[receiptKey] = { fingerprint, reply };
122
+ return reply;
123
+ }, { readOnly: observations.has(input.type) });
124
+ if (!observations.has(input.type)) this.emit('change');
125
+ return reply;
126
+ }
127
+ async receiveNotification(principal, message) {
128
+ if (principal.role !== 'device' || !message.session || !workflowTypes.has(message.type)) fail('FORBIDDEN', 'Unsupported downstream notification');
129
+ // Persist transport receipt and inbox entry together; this is NOT task completion.
130
+ return this.execute(principal, message, (state, p, input, emit) => {
131
+ state.localExecutions ||= {};
132
+ const key = queueKey(p, input.session), current = state.localExecutions[key];
133
+ if (input.type === 'task.assign') state.localExecutions[key] = { taskId: input.payload.taskId, mode: input.payload.mode || 'reviewed', nodeIds: input.payload.nodeIds, assignedAt: new Date().toISOString(), closed: false };
134
+ if (input.type === 'task.report' && current?.taskId === input.payload.taskId) {
135
+ if (['closed', 'finished'].includes(input.payload.stage)) current.closed = true;
136
+ if (input.payload.stage === 'planReady') { current.plan = { ref: input.payload.data.planRef, version: input.payload.data.planVersion, sourceSha: input.payload.data.sourceSha }; current.approval = null; }
137
+ }
138
+ if (input.type === 'review.result' && input.payload.kind === 'plan' && current?.plan?.ref === input.payload.ref && current.plan.version === input.payload.version) current.approval = input.payload.decision === 'approved' ? input.payload.receiptId : null;
139
+ if (input.type === 'review.result' && input.payload.kind === 'acceptance' && current && !current.closed &&
140
+ input.payload.ref.startsWith(`ci:${current.taskId}:`) && input.payload.receiptId) {
141
+ current.acceptanceReview = { ref: input.payload.ref, version: input.payload.version,
142
+ decision: input.payload.decision, reason: input.payload.reason, receiptId: input.payload.receiptId };
143
+ }
144
+ if (input.type === 'task.rework' && current?.taskId === input.payload.taskId) { current.plan = null; current.approval = null; current.acceptanceReview = null; }
145
+ emit(input); return { outcome: 'applied' };
146
+ });
147
+ }
148
+ async resumeControlApplied(principal, message) {
149
+ if (message.type !== 'task.control' || message.payload.action !== 'resume') return false;
150
+ return this.controlReportApplied(principal, message);
151
+ }
152
+ async controlReportApplied(principal, message) {
153
+ if (principal.role !== 'device') fail('FORBIDDEN', 'Only the receiving device can inspect delivery receipts');
154
+ if (message.type !== 'task.control' || !['resume', 'complete'].includes(message.payload.action)) return false;
155
+ await this.authorizeSession(principal, message.session);
156
+ return this.transaction(state => {
157
+ requireBinding(state, principal, message.session);
158
+ // Reuse the durable downlink journal, including receipts written before
159
+ // this guard existed. Do not require a second index or a data migration.
160
+ return queueFor(state, principal, message.session).items.some(({ message: received }) =>
161
+ received.type === 'task.report' && received.payload.stage === (message.payload.action === 'resume' ? 'resumed' : 'closed') &&
162
+ received.payload.taskId === message.payload.taskId && received.payload.data.controlId === message.id);
163
+ }, { readOnly: true });
164
+ }
165
+ async pendingControls(principal, session, action) {
166
+ if (principal.role !== 'device') fail('FORBIDDEN', 'Only the receiving device can inspect controls');
167
+ if (!['resume', 'complete'].includes(action)) fail('INVALID_ARGUMENT', 'Unsupported control action');
168
+ await this.authorizeSession(principal, session);
169
+ return this.transaction(state => {
170
+ requireBinding(state, principal, session);
171
+ const items = queueFor(state, principal, session).items;
172
+ const stage = action === 'resume' ? 'resumed' : 'closed';
173
+ const reports = new Set(items.filter(({ message }) => message.type === 'task.report' && message.payload.stage === stage)
174
+ .map(({ message }) => `${message.payload.taskId}:${message.payload.data.controlId}`));
175
+ const pending = items.filter(({ message }) => message.type === 'task.control' && message.payload.action === action &&
176
+ !reports.has(`${message.payload.taskId}:${message.id}`)).map(({ message }) => message);
177
+ if (action === 'complete') return pending;
178
+ const latest = new Map();
179
+ for (const message of pending) latest.set(message.payload.taskId, message);
180
+ return [...latest.values()];
181
+ }, { readOnly: true });
182
+ }
183
+ async requestSessionCreation(principal, input) {
184
+ requireIdentity(principal);
185
+ if (principal.role !== 'human' && !(principal.role === 'coordinator' && principal.creationTemplates?.includes(input?.templateSessionId))) fail('FORBIDDEN', 'Session creation requires an authorized project template');
186
+ if (!input || Object.keys(input).some(k => !['operationId', 'templateSessionId', 'name'].includes(k)) ||
187
+ ['operationId', 'templateSessionId', 'name'].some(k => typeof input[k] !== 'string' || !input[k].trim() || input[k].length > (k === 'name' ? 200 : 128))) fail('INVALID_ARGUMENT', 'Provide a stable request, configured template Session and name');
188
+ const result = await this.transaction(state => {
189
+ const template = state.bindings[bindingKey(principal, input.templateSessionId)];
190
+ if (!template) fail('NOT_FOUND', 'Template Session is not registered in this project');
191
+ state.sessionCreations ||= {};
192
+ const id = key([...principalKey(principal), 'session-create', input.operationId]);
193
+ const fingerprint = hash(canonical(input)), previous = state.sessionCreations[id];
194
+ if (previous) {
195
+ if (previous.fingerprint !== fingerprint) fail('ID_REUSED', 'Session creation request differs');
196
+ return previous.result;
197
+ }
198
+ const result = { id, operationId: input.operationId, sessionId: randomUUID(), templateSessionId: input.templateSessionId,
199
+ name: input.name.trim(), state: 'pending', createdAt: new Date().toISOString() };
200
+ state.sessionCreations[id] = { repositoryId: principal.repositoryId, deviceId: template.deviceId,
201
+ templateWorktreeId: template.worktreeId, fingerprint, result, requestedBy: { role: principal.role, agentId: principal.agentId } };
202
+ return result;
203
+ });
204
+ this.emit('change');
205
+ return result;
206
+ }
207
+ async retrySessionCreation(principal, id) {
208
+ requireIdentity(principal);
209
+ if (principal.role !== 'coordinator' || typeof id !== 'string' || !id) fail('FORBIDDEN', 'Coordinator creation retry required');
210
+ const result = await this.transaction(state => {
211
+ const creation = state.sessionCreations?.[id];
212
+ if (!creation || creation.repositoryId !== principal.repositoryId) fail('NOT_FOUND', 'Session creation is unavailable');
213
+ if (creation.requestedBy?.role !== 'coordinator' || creation.requestedBy.agentId !== principal.agentId ||
214
+ !principal.creationTemplates?.includes(creation.result.templateSessionId)) fail('FORBIDDEN', 'Creation belongs to another coordinator or template');
215
+ if (creation.result.state !== 'failed') return creation.result;
216
+ const { error, completedAt, ...rest } = creation.result;
217
+ creation.result = { ...rest, state: 'pending', retryCount: (rest.retryCount || 0) + 1,
218
+ lastError: error || 'SESSION_CREATION_FAILED', lastFailedAt: completedAt || null,
219
+ retryRequestedAt: new Date().toISOString() };
220
+ return creation.result;
221
+ });
222
+ this.emit('change');
223
+ return result;
224
+ }
225
+ async prepareProjectTask(principal, input, operationId, conversationId) {
226
+ requireIdentity(principal);
227
+ if (principal.role !== 'coordinator') fail('FORBIDDEN', 'Coordinator identity required');
228
+ return this.transaction(state => {
229
+ state.projectTasks ||= {};
230
+ const id = key([principal.repositoryId, input.taskId]);
231
+ const fingerprint = hash(canonical(input));
232
+ const previous = state.projectTasks[id];
233
+ if (previous) {
234
+ if (previous.conversationId !== conversationId) fail('CONFLICT', 'Project task belongs to another conversation');
235
+ if (previous.fingerprint === fingerprint) return previous;
236
+ if (previous.stage !== 'brief-rejected' || previous.operationId === operationId) fail('CONFLICT', 'Project task already has different requirements');
237
+ }
238
+ return state.projectTasks[id] = { ...structuredClone(input), repositoryId: principal.repositoryId,
239
+ conversationId, fingerprint, operationId, stage: 'brief', createdAt: new Date().toISOString(),
240
+ brief: { ref: `project-brief:${input.taskId}`, version: randomUUID() } };
241
+ });
242
+ }
243
+ async projectTasks(principal) {
244
+ requireIdentity(principal);
245
+ if (!['human', 'coordinator'].includes(principal.role)) fail('FORBIDDEN', 'Project task access required');
246
+ return this.transaction(state => Object.values(state.projectTasks || {}).filter(item => item.repositoryId === principal.repositoryId), { readOnly: true });
247
+ }
248
+ async projectTaskStatuses(principal) {
249
+ requireIdentity(principal);
250
+ if (!['human', 'coordinator'].includes(principal.role)) fail('FORBIDDEN', 'Project task access required');
251
+ return this.transaction(state => {
252
+ const executions = new Map(Object.values(state.tasks || {}).filter(task => task.repositoryId === principal.repositoryId)
253
+ .map(task => [key([task.id, task.session?.id]), task]));
254
+ return Object.values(state.projectTasks || {}).filter(item =>
255
+ item.repositoryId === principal.repositoryId && item.itemId && item.nodeId && ['todo', 'bug'].includes(item.kind))
256
+ .map(item => {
257
+ const execution = executions.get(key([item.taskId, item.sessionId]));
258
+ return { taskId: item.taskId, itemId: item.itemId, nodeId: item.nodeId, kind: item.kind,
259
+ sessionId: item.sessionId || null, state: execution?.stage || item.stage, projectStage: item.stage,
260
+ updatedAt: item.updatedAt || item.createdAt, ...(item.error ? { error: item.error } : {}) };
261
+ });
262
+ }, { readOnly: true });
263
+ }
264
+ async reviewProjectTask(principal, taskId, brief, input) {
265
+ requireIdentity(principal);
266
+ if (principal.role !== 'human' || !['approved', 'rejected'].includes(input.decision) || typeof input.id !== 'string' || !input.id) fail('FORBIDDEN', 'A human decision and stable receipt are required');
267
+ return this.transaction(state => {
268
+ const task = state.projectTasks?.[key([principal.repositoryId, taskId])];
269
+ if (!task || canonical(task.brief) !== canonical(brief)) fail('CONFLICT', 'Project requirements changed');
270
+ const review = { id: input.id, decision: input.decision, reason: input.reason || '' };
271
+ if (task.review) {
272
+ if (canonical(task.review) !== canonical(review)) fail('CONFLICT', 'Requirements already reviewed');
273
+ return task;
274
+ }
275
+ task.review = review; task.reviewIssuer = structuredClone(principal); task.stage = input.decision === 'approved' ? 'queued' : 'brief-rejected';
276
+ return task;
277
+ });
278
+ }
279
+ async updateProjectTask(principal, taskId, changes, { reserveLimit } = {}) {
280
+ requireIdentity(principal);
281
+ if (principal.role !== 'coordinator') fail('FORBIDDEN', 'Coordinator scheduler required');
282
+ return this.transaction(state => {
283
+ const task = state.projectTasks?.[key([principal.repositoryId, taskId])];
284
+ if (!task) fail('NOT_FOUND', 'Project task missing');
285
+ const transitions = { queued: ['creating'], creating: ['starting', 'failed'], starting: ['dispatched', 'queued', 'failed'], dispatched: ['completed'] };
286
+ if (changes.stage && changes.stage !== task.stage && !transitions[task.stage]?.includes(changes.stage)) return task;
287
+ if (reserveLimit !== undefined) {
288
+ if (task.stage !== 'queued') return task;
289
+ const active = Object.values(state.projectTasks).filter(item => item.repositoryId === principal.repositoryId && ['creating', 'starting', 'dispatched'].includes(item.stage)).length;
290
+ if (active >= reserveLimit) {
291
+ task.error = 'WAITING_CAPACITY'; task.updatedAt = new Date().toISOString(); return task;
292
+ }
293
+ }
294
+ Object.assign(task, structuredClone(changes), { updatedAt: new Date().toISOString() });
295
+ return task;
296
+ });
297
+ }
298
+ async pendingSessionCreations(principal) {
299
+ requireIdentity(principal);
300
+ if (principal.role !== 'device') fail('FORBIDDEN', 'Only the owning device can receive creation requests');
301
+ return this.transaction(state => Object.values(state.sessionCreations || {})
302
+ .filter(item => item.repositoryId === principal.repositoryId && item.deviceId === principal.deviceId && item.result.state === 'pending')
303
+ .slice(0, 20).map(item => item.result), { readOnly: true });
304
+ }
305
+ async sessionCreations(principal) {
306
+ requireIdentity(principal);
307
+ if (principal.role !== 'human') fail('FORBIDDEN', 'Only the project human can inspect creation records');
308
+ return this.transaction(state => Object.values(state.sessionCreations || {})
309
+ .filter(item => item.repositoryId === principal.repositoryId).map(item => item.result), { readOnly: true });
310
+ }
311
+ async creationTemplate(principal, sessionId) {
312
+ requireIdentity(principal);
313
+ if (principal.role !== 'device') fail('FORBIDDEN', 'Device identity required');
314
+ return this.transaction(state => {
315
+ const item = Object.values(state.sessionCreations || {}).find(item => item.repositoryId === principal.repositoryId &&
316
+ item.deviceId === principal.deviceId && item.result.sessionId === sessionId && item.result.state === 'registered');
317
+ if (!item) return null;
318
+ const binding = requireBinding(state, principal, { id: sessionId, generation: item.result.generation });
319
+ return binding.worktreeId === item.result.worktreeId ? item.result.templateSessionId : null;
320
+ }, { readOnly: true });
321
+ }
322
+ async finishSessionCreation(principal, input) {
323
+ requireIdentity(principal);
324
+ if (principal.role !== 'device') fail('FORBIDDEN', 'Only the owning device can report creation');
325
+ if (!input || Object.keys(input).some(k => !['id', 'error'].includes(k)) || typeof input.id !== 'string' ||
326
+ input.error !== undefined && (typeof input.error !== 'string' || !/^[A-Z][A-Z0-9_]{0,79}$/.test(input.error))) fail('INVALID_ARGUMENT', 'Provide the original creation ID and optional error code');
327
+ return this.transaction(state => {
328
+ const item = state.sessionCreations?.[input.id];
329
+ if (!item || item.repositoryId !== principal.repositoryId || item.deviceId !== principal.deviceId) fail('FORBIDDEN', 'Creation is not assigned to this device');
330
+ if (item.result.state !== 'pending') {
331
+ if (item.result.error !== input.error) fail('ID_REUSED', 'Creation outcome differs');
332
+ return item.result;
333
+ }
334
+ const binding = state.bindings[bindingKey(principal, item.result.sessionId)];
335
+ if (!input.error && (!binding || binding.deviceId !== principal.deviceId || binding.worktreeId === item.templateWorktreeId)) fail('CONFLICT', 'Register the new Session in an independent worktree before acknowledging');
336
+ item.result = { ...item.result, state: input.error ? 'failed' : 'registered',
337
+ ...(input.error ? { error: input.error } : { worktreeId: binding.worktreeId, generation: binding.generation }),
338
+ completedAt: new Date().toISOString() };
339
+ return item.result;
340
+ });
341
+ }
342
+ async submitApprovedTask(principal, request, resolveTask, workflow = {}) {
343
+ requireIdentity(principal);
344
+ if (!['human', 'coordinator'].includes(principal.role)) fail('FORBIDDEN', 'Human approval or approved project requirements required');
345
+ if (!request || typeof request.operationId !== 'string' || !request.operationId.trim() || request.operationId.length > 128) fail('INVALID_ARGUMENT', 'A stable operationId is required');
346
+ if (!request.session || typeof request.session.id !== 'string' || !Number.isSafeInteger(request.session.generation)) fail('INVALID_ARGUMENT', 'A bound Session is required');
347
+ return this.transaction(async state => {
348
+ const binding = requireBinding(state, principal, request.session);
349
+ const projectTask = request.projectTaskId && state.projectTasks?.[key([principal.repositoryId, request.projectTaskId])];
350
+ if (principal.role === 'coordinator' && (!projectTask || projectTask.review?.decision !== 'approved' || projectTask.sessionId !== request.session.id)) fail('FORBIDDEN', 'Project approval does not authorize this execution Session');
351
+ state.taskDispatches ||= {};
352
+ const dispatchKey = key([...principalKey(principal), 'task-dispatch', request.operationId]);
353
+ const fingerprint = hash(canonical(request));
354
+ const prior = state.taskDispatches[dispatchKey];
355
+ if (prior) {
356
+ if (prior.fingerprint !== fingerprint) fail('ID_REUSED', 'Task dispatch ID already has different content');
357
+ return prior.result;
358
+ }
359
+ const resolved = await resolveTask();
360
+ if (projectTask && (resolved.taskId !== projectTask.taskId || canonical(resolved.nodeIds) !== canonical(projectTask.nodeIds) || resolved.mainVersion !== projectTask.mainVersion || resolved.mode && resolved.mode !== 'reviewed' || resolved.text !== JSON.stringify({ v: 1, taskId: projectTask.taskId, text: projectTask.text, acceptance: projectTask.acceptance, nodeIds: projectTask.nodeIds, mainVersion: projectTask.mainVersion }))) fail('CONFLICT', 'Dispatch differs from approved project requirements');
361
+ const taskId = resolved.taskId;
362
+ const coordinator = { ...principal, deviceId: 'cloud-service', agentId: 'cloud-coordinator', role: 'coordinator', bindings: { [request.session.id]: binding.worktreeId } };
363
+ const prefix = `dispatch:${hash(canonical([principal.repositoryId, request.operationId])).slice(0, 32)}`;
364
+ const emitted = message => {
365
+ validateMessage(message);
366
+ const queue = queueFor(state, principal, request.session);
367
+ const previous = queue.items.find(item => item.message.id === message.id);
368
+ if (previous) return previous.seq;
369
+ const seq = ++queue.latestSeq;
370
+ queue.items.push({ seq, message: structuredClone(message) });
371
+ return seq;
372
+ };
373
+ const brief = await reduceWorkflow(state, coordinator, { v: 2, id: `${prefix}:brief`, type: 'brief.submit', session: request.session, payload: { taskId, text: resolved.text } }, emitted, workflow);
374
+ await reduceWorkflow(state, coordinator, { v: 2, id: `${prefix}:brief-review`, type: 'review.request', session: request.session, payload: { kind: 'brief', ref: brief.ref, version: brief.version, taskId } }, emitted, workflow);
375
+ await reduceWorkflow(state, projectTask?.reviewIssuer || principal, { v: 2, id: `${prefix}:approved`, type: 'review.result', session: request.session, payload: { kind: 'brief', ref: brief.ref, version: brief.version, decision: 'approved', reason: projectTask?.review.reason || '用户在 Cloud 工作台确认分配' } }, emitted, workflow);
376
+ const assigned = await reduceWorkflow(state, coordinator, { v: 2, id: `${prefix}:assign`, type: 'task.assign', session: request.session, payload: { taskId, briefRef: brief.ref, briefVersion: brief.version, sessionId: request.session.id, nodeIds: resolved.nodeIds, mainVersion: resolved.mainVersion, ...(resolved.mode ? { mode: resolved.mode } : {}) } }, emitted, workflow);
377
+ const result = { deliveryId: request.operationId, taskId, sessionId: request.session.id, state: assigned.stage === 'queued' ? 'queued' : 'cloud_queued', taskVersion: assigned.version };
378
+ state.taskDispatches[dispatchKey] = { fingerprint, result };
379
+ return result;
380
+ }).then(result => { this.emit('change'); return result; });
381
+ }
382
+ async taskStatus(principal, session, taskId) {
383
+ requireIdentity(principal);
384
+ return this.transaction(state => {
385
+ requireBinding(state, principal, session);
386
+ let task = state.tasks[scopedObjectKey(principal, session, `task:${taskId}`)];
387
+ if (!task && principal.role === 'human') {
388
+ const older = Object.values(state.tasks).filter(value => value.repositoryId === principal.repositoryId && value.session.id === session.id && value.id === taskId && value.stage === 'finished');
389
+ if (older.length === 1) task = older[0];
390
+ }
391
+ if (!task) fail('NOT_FOUND', 'Task is not registered in this Session');
392
+ const queue = queueFor(state, principal, task.session);
393
+ const outcomes = task.assignmentSeq
394
+ ? Object.values(queue.consumers || {}).map(consumer => consumer.outcomes?.[task.assignmentSeq]).filter(Boolean)
395
+ : [];
396
+ const delivered = outcomes.find(item => item.deliveryState === 'received') || outcomes.find(item => item.deliveryState) || outcomes[0];
397
+ const blockingTask = task.stage === 'queued'
398
+ ? Object.values(state.tasks).find(value => value.repositoryId === principal.repositoryId && canonical(value.session) === canonical(task.session) && value.id !== task.id && value.busy)
399
+ : null;
400
+ const stateName = task.stage === 'finished' ? (task.result?.outcome === 'success' ? 'completed' : task.result?.outcome || 'unknown') : task.stage === 'queued' ? 'queued'
401
+ : ['plan-ready', 'plan-rejected'].includes(task.stage) ? 'waiting_review'
402
+ : task.stage === 'executing' ? 'executing'
403
+ : task.stage !== 'assigned' ? task.stage
404
+ : delivered?.deliveryState === 'received' ? 'codex_received'
405
+ : delivered?.deliveryState === 'uncertain' ? 'uncertain'
406
+ : delivered ? 'local_received' : 'cloud_queued';
407
+ return { taskId: task.id, sessionId: session.id, state: stateName, stage: task.stage, version: task.version,
408
+ ...(blockingTask ? { queue: { reason: 'executor-busy', blockedByTaskId: blockingTask.id } } : {}),
409
+ ...(task.result ? { result: { outcome: task.result.outcome, summary: task.result.summary, finishedAt: task.result.finishedAt } } : {}) };
410
+ }, { readOnly: true });
411
+ }
412
+ async taskRecord(principal, session, taskId) {
413
+ requireIdentity(principal);
414
+ return this.transaction(state => {
415
+ requireBinding(state, principal, session);
416
+ const task = state.tasks[scopedObjectKey(principal, session, `task:${taskId}`)];
417
+ if (!task) fail('NOT_FOUND', 'Task is not registered in this Session');
418
+ return structuredClone(task);
419
+ }, { readOnly: true });
420
+ }
421
+ async workflowTasks(principal, session) {
422
+ requireIdentity(principal);
423
+ return this.transaction(state => {
424
+ requireBinding(state, principal, session);
425
+ return Object.values(state.tasks).filter(task => task.repositoryId === principal.repositoryId && canonical(task.session) === canonical(session));
426
+ }, { readOnly: true });
427
+ }
428
+ async humanTaskResult(principal, sessionId, taskId) {
429
+ requireIdentity(principal);
430
+ if (principal.role !== 'human') fail('FORBIDDEN', 'Human review requires browser authority');
431
+ return this.transaction(state => {
432
+ const tasks = Object.values(state.tasks).filter(task => task.repositoryId === principal.repositoryId && task.session.id === sessionId && task.id === taskId);
433
+ if (tasks.length !== 1) fail('NOT_FOUND', 'Task result is missing or ambiguous');
434
+ const task = tasks[0];
435
+ if (task.stage !== 'finished' || !task.result) fail('CONFLICT', 'Agent has not submitted a final result');
436
+ return { taskId, sessionId, version: task.version, result: structuredClone(task.result) };
437
+ }, { readOnly: true });
438
+ }
439
+ async handle(principal, input, options = {}) {
440
+ return this.execute(principal, input, async (state, p, message, emit) => {
441
+ const payload = message.payload;
442
+ if (message.type === 'workbench.read' && options.workbenchRead) return options.workbenchRead(p, message);
443
+ if (workflowTypes.has(message.type)) {
444
+ const taskId = payload?.taskId;
445
+ if (taskId) {
446
+ const task = state.tasks[scopedObjectKey(p, message.session, `task:${taskId}`)];
447
+ const projectTask = Object.values(state.projectTasks || {}).find(item => item.taskId === taskId);
448
+ if (task && projectTask) {
449
+ const brief = task.brief && state.objects[scopedObjectKey(p, message.session, task.brief.ref)]?.versions?.[task.brief.version]?.content;
450
+ const text = `${projectTask.text || ''} ${projectTask.acceptance || ''} ${brief?.text || ''}`;
451
+ task.verificationOnly = projectTask.verificationOnly === true || /链路验证|只读|不修改(?:业务)?(?:文件|代码)|无业务(?:文件|代码)|不改(?:动)?业务/.test(text);
452
+ }
453
+ }
454
+ return reduceWorkflow(state, p, message, emit, options.workflow);
455
+ }
456
+ if (message.type === 'blob.put' && options.blobs) return options.blobs.register(p, message.session, payload);
457
+ if (message.type === 'blob.get' && options.blobs) return options.blobs.metadata(p, message.session, payload.blobId);
458
+ if (message.type === 'session.bind') {
459
+ if (!await options.verifyBinding?.(p, payload)) fail('FORBIDDEN', 'Binding requires a verified local registration');
460
+ const id = bindingKey(p, payload.sessionId), previous = state.bindings[id];
461
+ const same = previous && previous.deviceId === p.deviceId && previous.agentId === payload.agentId && previous.worktreeId === payload.worktreeId;
462
+ if (p.role !== 'device' && payload.agentId !== p.agentId) fail('FORBIDDEN', 'Agent identity differs from credential');
463
+ if (same) {
464
+ if (payload.expectedBindingVersion && payload.expectedBindingVersion !== previous.version) fail('CONFLICT', 'Binding changed', { currentVersion: previous.version });
465
+ previous.sessionId = payload.sessionId;
466
+ return { session: { id: payload.sessionId, generation: previous.generation }, bindingVersion: previous.version };
467
+ }
468
+ if (previous && (!options.allowMigration || previous.deviceId !== p.deviceId || payload.expectedBindingVersion !== previous.version)) fail('CONFLICT', 'Migration requires the owning device and current binding version', { currentVersion: previous.version });
469
+ if (!previous && payload.expectedBindingVersion) fail('CONFLICT', 'Binding does not exist', { currentVersion: '' });
470
+ const binding = { sessionId: payload.sessionId, deviceId: p.deviceId, agentId: payload.agentId, worktreeId: payload.worktreeId, generation: (previous?.generation || 0) + 1, version: randomUUID() };
471
+ for (const creation of Object.values(state.sessionCreations || {})) {
472
+ if (creation.repositoryId !== p.repositoryId || creation.result.sessionId !== payload.sessionId || creation.result.state !== 'pending') continue;
473
+ if (creation.deviceId !== p.deviceId || creation.templateWorktreeId === binding.worktreeId) fail('FORBIDDEN', 'Create this Session only on its assigned device and an independent worktree');
474
+ creation.result = { ...creation.result, state: 'registered', worktreeId: binding.worktreeId,
475
+ generation: binding.generation, completedAt: new Date().toISOString() };
476
+ }
477
+ state.bindings[id] = binding;
478
+ queueFor(state, p, { id: payload.sessionId, generation: binding.generation });
479
+ return { session: { id: payload.sessionId, generation: binding.generation }, bindingVersion: binding.version };
480
+ }
481
+ if (message.type === 'sync.heartbeat') {
482
+ const sessions = [], rejected = [];
483
+ let firstError;
484
+ for (const s of payload.sessions) {
485
+ try {
486
+ requireBinding(state, p, s);
487
+ const queue = queueFor(state, p, s);
488
+ const consumer = consumerFor(queue, p);
489
+ if (s.ackedSeq > consumer.ackedSeq) fail('CONFLICT', 'Client acknowledgement is ahead of durable server state');
490
+ sessions.push({ id: s.id, generation: s.generation, latestSeq: queue.latestSeq, ackedSeq: consumer.ackedSeq });
491
+ } catch (error) {
492
+ if (!['FORBIDDEN', 'STALE_SESSION', 'CONFLICT'].includes(error.code)) throw error;
493
+ firstError ||= error;
494
+ rejected.push({ id: s.id, generation: s.generation, code: error.code });
495
+ }
496
+ }
497
+ if (!sessions.length && firstError) throw firstError;
498
+ return { sessions, ...(rejected.length ? { rejected } : {}) };
499
+ }
500
+ if (message.type === 'sync.read') {
501
+ const queue = queueFor(state, p, message.session);
502
+ if (payload.afterSeq > queue.latestSeq) fail('INVALID_ARGUMENT', 'Read cursor is ahead of the queue');
503
+ const messages = []; let size = 512;
504
+ for (const item of queue.items) {
505
+ if (item.seq <= payload.afterSeq) continue;
506
+ const bytes = Buffer.byteLength(JSON.stringify(item)) + 1;
507
+ if (messages.length >= payload.limit || size + bytes > MAX_MESSAGE_BYTES) break;
508
+ messages.push(item); size += bytes;
509
+ }
510
+ const nextSeq = messages.at(-1)?.seq ?? payload.afterSeq;
511
+ return { messages, nextSeq, hasMore: nextSeq < queue.latestSeq };
512
+ }
513
+ if (message.type === 'sync.ack') {
514
+ const queue = queueFor(state, p, message.session);
515
+ const consumer = consumerFor(queue, p, true);
516
+ for (const item of payload.items) {
517
+ if (item.seq > queue.latestSeq) fail('INVALID_ARGUMENT', 'Cannot acknowledge an unsent sequence');
518
+ const prior = consumer.outcomes[item.seq];
519
+ if (prior && canonical(prior) !== canonical(item)) fail('CONFLICT', 'Acknowledged outcome is immutable');
520
+ consumer.outcomes[item.seq] = item;
521
+ }
522
+ while (consumer.outcomes[consumer.ackedSeq + 1]) consumer.ackedSeq++;
523
+ return { ackedSeq: consumer.ackedSeq };
524
+ }
525
+ if (message.type === 'object.put' || message.type === 'object.read') {
526
+ const objectKey = scopedObjectKey(p, message.session, payload.ref);
527
+ const record = state.objects[objectKey];
528
+ if (message.type === 'object.read') {
529
+ const saved = record?.versions[payload.version];
530
+ if (!saved) fail('NOT_FOUND', 'Object version does not exist in this Session');
531
+ return { ref: payload.ref, version: payload.version, kind: saved.kind, content: saved.content };
532
+ }
533
+ if ((record?.latest || '') !== payload.baseVersion) fail('CONFLICT', 'Object changed', { currentVersion: record?.latest || '' });
534
+ if (record && record.versions[record.latest].kind !== payload.kind) fail('CONFLICT', 'Object kind cannot change');
535
+ const version = randomUUID(), target = record || { latest: '', versions: {} };
536
+ target.versions[version] = { kind: payload.kind, content: structuredClone(payload.content) };
537
+ target.latest = version; state.objects[objectKey] = target;
538
+ return { ref: payload.ref, version };
539
+ }
540
+ fail('INVALID_ARGUMENT', 'This message is not implemented by this endpoint yet');
541
+ }, options.authorize);
542
+ }
543
+ async authorizeSession(principal, session) {
544
+ requireIdentity(principal);
545
+ return this.transaction(state => requireBinding(state, principal, session), { readOnly: true });
546
+ }
547
+ async recoverySnapshot(principal, session, load) {
548
+ requireIdentity(principal);
549
+ return this.transaction(async state => {
550
+ requireBinding(state, principal, session);
551
+ // Writers cannot append or acknowledge tasks while the Map snapshot is
552
+ // captured. Later writes stay beyond this barrier and are read normally.
553
+ const source = await load();
554
+ const queue = queueFor(state, principal, session), consumer = consumerFor(queue, principal);
555
+ const pendingMessages = queue.items.filter(item => !consumer.outcomes[item.seq]);
556
+ const recovery = { resumeAfterSeq: queue.latestSeq, pendingMessages };
557
+ return { ...source, mapVersion: source.version, version: hash(canonical([source.version, recovery])), recovery };
558
+ });
559
+ }
560
+ async activeExecution(principal, session) {
561
+ await this.authorizeSession(principal, session);
562
+ return this.transaction(state => {
563
+ requireBinding(state, principal, session);
564
+ const current = state.localExecutions?.[queueKey(principal, session)];
565
+ if (!current || current.closed) return null;
566
+ if (current.acceptanceReview !== undefined) return current;
567
+ // Existing receivers may have acknowledged acceptance before the local
568
+ // projection included it. Rebuild only from the authenticated durable
569
+ // downlink journal; never trust a model-supplied review claim.
570
+ const accepted = [...(state.queues?.[queueKey(principal, session)]?.items || [])].reverse()
571
+ .map(item => item.message).find(message => message.type === 'review.result' &&
572
+ message.payload.kind === 'acceptance' && message.payload.receiptId &&
573
+ message.payload.ref.startsWith(`ci:${current.taskId}:`));
574
+ if (!accepted) return current;
575
+ const { ref, version, decision, reason, receiptId } = accepted.payload;
576
+ return { ...current, acceptanceReview: { ref, version, decision, reason, receiptId } };
577
+ }, { readOnly: true });
578
+ }
579
+ async executionReport(principal, session, { deliveryId, stage, summary, outcome = 'success' }) {
580
+ return this.transaction(state => {
581
+ requireBinding(state, principal, session);
582
+ const message = queueFor(state, principal, session).items.find(item => item.message.id === deliveryId)?.message;
583
+ if (!message || message.type !== 'task.assign' || message.payload.mode !== 'session') fail('NOT_FOUND', 'No Session task matches this delivery');
584
+ if (!['started', 'finished'].includes(stage)) fail('INVALID_ARGUMENT', 'Expected start or finish');
585
+ return validateMessage({ v: 2, id: `${deliveryId}:${stage}`, type: 'task.report', session,
586
+ payload: { taskId: message.payload.taskId, stage, data: { deliveryId, ...(stage === 'finished' ? { outcome, summary } : {}) } } });
587
+ }, { readOnly: true });
588
+ }
589
+ async registeredBinding(principal, sessionId) {
590
+ requireIdentity(principal);
591
+ return this.transaction(state => {
592
+ const binding = state.bindings[bindingKey(principal, sessionId)];
593
+ if (!binding) return null;
594
+ requireBinding(state, principal, { id: sessionId, generation: binding.generation });
595
+ return binding;
596
+ }, { readOnly: true });
597
+ }
598
+ async rememberSessionNames(principal, sessions) {
599
+ requireIdentity(principal);
600
+ const update = state => {
601
+ for (const session of sessions) {
602
+ const binding = requireBinding(state, principal, session);
603
+ if (session.name) binding.name = session.name;
604
+ if (session.platform) binding.platform = session.platform;
605
+ }
606
+ };
607
+ const state = await this.immutableState();
608
+ if (!sessions.some(session => {
609
+ const binding = requireBinding(state, principal, session);
610
+ return session.name && session.name !== binding.name || session.platform && session.platform !== binding.platform;
611
+ })) return;
612
+ await this.transaction(update);
613
+ }
614
+ async queueHeads(principal) {
615
+ requireIdentity(principal);
616
+ return this.transaction(state => Object.entries(state.bindings).flatMap(([id, binding]) => {
617
+ // Queue keys are opaque, so find the Session ID persisted on its binding.
618
+ if (!binding.sessionId || id !== bindingKey(principal, binding.sessionId)) return [];
619
+ const session = { id: binding.sessionId, generation: binding.generation };
620
+ try { requireBinding(state, principal, session); } catch { return []; }
621
+ return [{ session, latestSeq: state.queues[queueKey(principal, session)]?.latestSeq || 0 }];
622
+ }), { readOnly: true });
623
+ }
624
+ }