@agentunion/fastaun 0.5.20 → 0.5.22

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 (128) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/_packed_docs/CHANGELOG.md +104 -0
  3. package/_packed_docs/INDEX.md +59 -34
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +59 -52
  5. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  7. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  8. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  9. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +307 -67
  10. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +41 -15
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
  14. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +33 -22
  16. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  17. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  18. package/_packed_docs/sdk/0.5.22-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +212 -0
  19. package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +199 -0
  20. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  21. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  22. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  23. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  24. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  25. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -14
  26. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  27. package/_packed_docs/sdk/09-group-rpc-manual.md +15 -2
  28. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  29. package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
  30. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +73 -0
  31. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +90 -0
  32. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +33 -25
  33. package/_packed_docs/sdk/INDEX.md +67 -33
  34. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +74 -57
  35. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +312 -300
  36. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +322 -382
  37. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -708
  38. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +28 -20
  39. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +190 -97
  40. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +158 -127
  41. package/dist/agent-md-schema.js +6 -4
  42. package/dist/agent-md-schema.js.map +1 -1
  43. package/dist/agent-md.d.ts +22 -2
  44. package/dist/agent-md.js +92 -68
  45. package/dist/agent-md.js.map +1 -1
  46. package/dist/aid-store.d.ts +13 -0
  47. package/dist/aid-store.js +50 -15
  48. package/dist/aid-store.js.map +1 -1
  49. package/dist/auth.d.ts +1 -0
  50. package/dist/auth.js +22 -2
  51. package/dist/auth.js.map +1 -1
  52. package/dist/cert-utils.d.ts +1 -0
  53. package/dist/cert-utils.js +13 -0
  54. package/dist/cert-utils.js.map +1 -1
  55. package/dist/client/delivery.d.ts +87 -1
  56. package/dist/client/delivery.js +1387 -139
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/group-state.js +3 -0
  59. package/dist/client/group-state.js.map +1 -1
  60. package/dist/client/rpc-pipeline.d.ts +4 -1
  61. package/dist/client/rpc-pipeline.js +149 -37
  62. package/dist/client/rpc-pipeline.js.map +1 -1
  63. package/dist/client/v2-e2ee.d.ts +12 -2
  64. package/dist/client/v2-e2ee.js +831 -304
  65. package/dist/client/v2-e2ee.js.map +1 -1
  66. package/dist/client.d.ts +26 -2
  67. package/dist/client.js +412 -68
  68. package/dist/client.js.map +1 -1
  69. package/dist/config.d.ts +4 -0
  70. package/dist/config.js +1 -0
  71. package/dist/config.js.map +1 -1
  72. package/dist/events.d.ts +18 -1
  73. package/dist/events.js +148 -9
  74. package/dist/events.js.map +1 -1
  75. package/dist/facades.d.ts +43 -1
  76. package/dist/facades.js +363 -7
  77. package/dist/facades.js.map +1 -1
  78. package/dist/group-fs.d.ts +1 -0
  79. package/dist/group-fs.js +37 -19
  80. package/dist/group-fs.js.map +1 -1
  81. package/dist/index.d.ts +5 -3
  82. package/dist/index.js +1 -1
  83. package/dist/index.js.map +1 -1
  84. package/dist/keystore/aid-db.d.ts +6 -1
  85. package/dist/keystore/aid-db.js +184 -42
  86. package/dist/keystore/aid-db.js.map +1 -1
  87. package/dist/keystore/index.d.ts +14 -0
  88. package/dist/keystore/local-token-store.d.ts +9 -1
  89. package/dist/keystore/local-token-store.js +79 -14
  90. package/dist/keystore/local-token-store.js.map +1 -1
  91. package/dist/org-protocol.d.ts +46 -0
  92. package/dist/org-protocol.js +802 -12
  93. package/dist/org-protocol.js.map +1 -1
  94. package/dist/ownership-v21.d.ts +36 -5
  95. package/dist/ownership-v21.js +168 -52
  96. package/dist/ownership-v21.js.map +1 -1
  97. package/dist/ownership-workflow.d.ts +24 -2
  98. package/dist/ownership-workflow.js +682 -41
  99. package/dist/ownership-workflow.js.map +1 -1
  100. package/dist/secret-store/file-store.js +84 -0
  101. package/dist/secret-store/file-store.js.map +1 -1
  102. package/dist/seq-tracker.d.ts +1 -0
  103. package/dist/seq-tracker.js +2 -2
  104. package/dist/seq-tracker.js.map +1 -1
  105. package/dist/storage/lowlevel.d.ts +6 -0
  106. package/dist/storage/lowlevel.js +24 -7
  107. package/dist/storage/lowlevel.js.map +1 -1
  108. package/dist/storage/read-cache.d.ts +70 -0
  109. package/dist/storage/read-cache.js +625 -0
  110. package/dist/storage/read-cache.js.map +1 -0
  111. package/dist/storage/vfs.d.ts +2 -0
  112. package/dist/storage/vfs.js +50 -15
  113. package/dist/storage/vfs.js.map +1 -1
  114. package/dist/tools/cross-sdk-agent.d.ts +86 -3
  115. package/dist/tools/cross-sdk-agent.js +758 -46
  116. package/dist/tools/cross-sdk-agent.js.map +1 -1
  117. package/dist/transport.d.ts +3 -0
  118. package/dist/transport.js +104 -46
  119. package/dist/transport.js.map +1 -1
  120. package/dist/types.d.ts +1 -0
  121. package/dist/v2/e2ee/encrypt-group.js +1 -0
  122. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  123. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  124. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  125. package/dist/v2/e2ee/types.d.ts +1 -0
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/package.json +1 -1
@@ -3,6 +3,13 @@ import { isScalar, parseDocument } from 'yaml';
3
3
  import { StateError, ValidationError } from './errors.js';
4
4
  import { OWNERSHIP_UUID_V4_RE, ownershipAid, ownershipFailure, ownershipInteger, ownershipSha256, parseOwnershipFile, requireOwnershipApplicable } from './ownership-v21.js';
5
5
  export const OWNERSHIP_SDK_MESSAGE_TYPE = 'aun.ownership.sdk.v1';
6
+ class OwnershipTaskDecodeError extends StateError {
7
+ operationId;
8
+ constructor(operationId, reason) {
9
+ super(reason, { stringCode: 'persistence_unavailable' });
10
+ this.operationId = operationId;
11
+ }
12
+ }
6
13
  function participants(task) {
7
14
  return [...new Set([task.owner_aid, task.sub_aid, ...(task.new_owner_aid ? [task.new_owner_aid] : [])])];
8
15
  }
@@ -43,7 +50,8 @@ export function parseOwnershipMessage(content, context) {
43
50
  ownershipAid(message.sub_aid, 'sub_aid');
44
51
  ownershipAid(message.owner_aid, 'owner_aid');
45
52
  ownershipInteger(message.base_relation_version, 'base_relation_version', 0);
46
- if (ownershipInteger(message.target_relation_version, 'target_relation_version') !== message.base_relation_version + (message.operation === 'release' ? 0 : 1))
53
+ const target = ownershipInteger(message.target_relation_version, 'target_relation_version');
54
+ if (target !== message.base_relation_version + 1 && !(message.operation === 'release' && target === message.base_relation_version))
47
55
  throw new ValidationError('ownership target version does not match the operation');
48
56
  if (message.operation === 'transfer') {
49
57
  if (ownershipAid(message.new_owner_aid, 'new_owner_aid') === message.owner_aid)
@@ -106,8 +114,8 @@ export function parseOwnershipMessage(content, context) {
106
114
  const ns = context.ns;
107
115
  if (ns.protocol !== '2.1')
108
116
  throw ownershipFailure('unsupported_protocol', 'NS ownership protocol is unavailable');
109
- const base = ns.relation_version === message.base_relation_version && (message.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === message.owner_aid);
110
- const target = message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === message.target_relation_version || ns.relation_version === 0) : ns.relation_version === message.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(message);
117
+ const base = ns.relation_version === message.base_relation_version && (message.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === message.owner_aid) || message.operation === 'bind' && ns.relation_version === message.target_relation_version && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending');
118
+ const target = message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === message.base_relation_version || ns.relation_version === 0) : ns.relation_version === message.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(message);
111
119
  if (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target : !base && !target)
112
120
  throw ownershipFailure('conflict', 'ownership current NS relation does not match this task');
113
121
  }
@@ -166,6 +174,95 @@ export class OwnershipWorkflow {
166
174
  throw ownershipFailure('invalid', 'ownership rejection reason must be a string');
167
175
  return this.decide(operationId, 'reject', reason);
168
176
  }
177
+ async resetOwnershipTask() {
178
+ if (!this.deps.store.loadOwnershipTaskSource || !this.deps.store.resetOwnershipTask)
179
+ throw ownershipFailure('persistence_unavailable', 'atomic ownership task reset storage is unavailable');
180
+ const local = this.local(), check = () => { if (this.local() !== local)
181
+ throw ownershipFailure('conflict', 'ownership reset identity changed'); };
182
+ const reset = [], skipped = [];
183
+ for (const snapshot of this.list(true)) {
184
+ check();
185
+ if (!OWNERSHIP_UUID_V4_RE.test(snapshot.operation_id)) {
186
+ skipped.push(snapshot.operation_id);
187
+ continue;
188
+ }
189
+ let source = this.taskSource(snapshot.operation_id);
190
+ for (let attempt = 0; source; attempt++) {
191
+ let task;
192
+ try {
193
+ const parsed = JSON.parse(source.source);
194
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
195
+ task = parsed;
196
+ }
197
+ catch { }
198
+ if (task && Object.hasOwn(task, 'task_reset')) {
199
+ try {
200
+ this.decode({ operation_id: source.operation_id, revision: source.revision, task });
201
+ }
202
+ catch {
203
+ skipped.push(snapshot.operation_id);
204
+ }
205
+ break;
206
+ }
207
+ let initiator;
208
+ try {
209
+ initiator = ownershipAid(task?.initiator_aid, 'initiator_aid');
210
+ }
211
+ catch { }
212
+ if (!task || !initiator || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(task.status)) {
213
+ skipped.push(snapshot.operation_id);
214
+ break;
215
+ }
216
+ if (initiator !== local || ['completed', 'rejected', 'stopped'].includes(task.status))
217
+ break;
218
+ let subject;
219
+ try {
220
+ subject = ownershipAid(task.sub_aid, 'sub_aid');
221
+ }
222
+ catch { }
223
+ try {
224
+ const original = source, result = await this.runSerialized(subject ?? source.operation_id, () => { check(); return this.resetSource(original, subject); });
225
+ check();
226
+ if (result.created)
227
+ reset.push(source.operation_id);
228
+ break;
229
+ }
230
+ catch (error) {
231
+ check();
232
+ if (attempt > 0 || !(error instanceof StateError) || error.stringCode !== 'conflict')
233
+ throw error;
234
+ source = this.taskSource(snapshot.operation_id);
235
+ }
236
+ }
237
+ }
238
+ check();
239
+ return { reset_operation_ids: [...new Set(reset)].sort(), skipped_operation_ids: [...new Set(skipped)].sort() };
240
+ }
241
+ async resetTask(operationId) {
242
+ if (typeof operationId !== 'string' || !OWNERSHIP_UUID_V4_RE.test(operationId))
243
+ throw ownershipFailure('invalid', 'ownership operation_id is invalid');
244
+ const source = this.taskSource(operationId);
245
+ if (!source)
246
+ throw ownershipFailure('operation_not_found', 'no local ownership task matches operation_id');
247
+ let task = {};
248
+ try {
249
+ const parsed = JSON.parse(source.source);
250
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
251
+ task = parsed;
252
+ }
253
+ catch { }
254
+ if (Object.hasOwn(task, 'task_reset')) {
255
+ this.decode({ operation_id: operationId, revision: source.revision, task });
256
+ return { operation_id: operationId, reset: true };
257
+ }
258
+ let subject;
259
+ try {
260
+ subject = ownershipAid(task.sub_aid, 'sub_aid');
261
+ }
262
+ catch { }
263
+ await this.runSerialized(subject ?? operationId, () => this.resetSource(source, subject));
264
+ return { operation_id: operationId, reset: true };
265
+ }
169
266
  async checkOwnershipConsensus(subAid = this.local()) {
170
267
  return this.deps.files.checkConsensus(ownershipAid(subAid, 'sub_aid'));
171
268
  }
@@ -188,8 +285,12 @@ export class OwnershipWorkflow {
188
285
  return true;
189
286
  let message;
190
287
  try {
288
+ const saved = this.load(String(body.operation_id));
289
+ if (saved && this.isReset(saved.record))
290
+ return true;
291
+ const task = saved && !saved.record.base_agent_md && saved.record.request_verified && !['completed', 'stopped', 'rejected'].includes(saved.record.status) ? this.legacyTaskView(saved.record) : saved?.record;
191
292
  message = parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(),
192
- task: ['bind.request', 'transfer.request'].includes(String(body.action)) || body.operation === 'release' && body.action === 'relation.confirmed' ? undefined : this.load(String(body.operation_id))?.record });
293
+ task: task ?? (['bind.request', 'transfer.request'].includes(String(body.action)) || body.operation === 'release' && body.action === 'relation.confirmed' ? undefined : saved?.record) });
193
294
  }
194
295
  catch (error) {
195
296
  if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
@@ -209,12 +310,14 @@ export class OwnershipWorkflow {
209
310
  return this.runSerialized(message.sub_aid, async () => {
210
311
  let handle = this.load(message.operation_id);
211
312
  if (handle) {
313
+ const task = !handle.record.base_agent_md && handle.record.request_verified && !['completed', 'stopped', 'rejected'].includes(handle.record.status) ? this.legacyTaskView(handle.record) : handle.record;
212
314
  try {
213
- parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(), task: handle.record });
315
+ parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(), task });
214
316
  }
215
317
  catch {
216
318
  return true;
217
319
  }
320
+ await this.upgradeLegacy(handle);
218
321
  }
219
322
  else {
220
323
  if (!['bind.request', 'transfer.request'].includes(message.action) && !(message.operation === 'release' && message.action === 'relation.confirmed'))
@@ -249,6 +352,10 @@ export class OwnershipWorkflow {
249
352
  for (const snapshot of this.list(true)) {
250
353
  try {
251
354
  const record = this.decode(snapshot).record;
355
+ if (this.isReset(record))
356
+ continue;
357
+ if (!record.base_agent_md && record.request_verified && ['completed', 'stopped', 'rejected'].includes(record.status))
358
+ continue;
252
359
  await this.runSerialized(record.sub_aid, async () => {
253
360
  const handle = this.load(record.operation_id);
254
361
  await this.drive(handle);
@@ -290,11 +397,116 @@ export class OwnershipWorkflow {
290
397
  throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
291
398
  }
292
399
  }
400
+ taskSource(operationId) {
401
+ try {
402
+ if (!this.deps.store.loadOwnershipTaskSource || !this.deps.store.resetOwnershipTask)
403
+ throw new Error('atomic ownership task reset storage is unavailable');
404
+ return this.deps.store.loadOwnershipTaskSource(this.local(), operationId);
405
+ }
406
+ catch (error) {
407
+ throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
408
+ }
409
+ }
410
+ async resetSource(source, subject) {
411
+ const task = { protocol: '2.1', operation_id: source.operation_id, local_aid: this.local(), status: 'stopped', step: 'reset',
412
+ ...(subject ? { sub_aid: subject } : {}), task_reset: { source: source.source, source_sha256: ownershipSha256(source.source), revision: source.revision, reset_at: Math.trunc(Date.now() / 1000) } };
413
+ try {
414
+ const saved = this.deps.store.resetOwnershipTask(this.local(), source.operation_id, source.revision, source.source, task);
415
+ this.decode(saved);
416
+ if (!this.isReset(saved.task))
417
+ throw new Error('ownership reset marker was not saved');
418
+ const committed = this.taskSource(source.operation_id);
419
+ if (!committed || committed.revision !== saved.revision || JSON.stringify(JSON.parse(committed.source)) !== JSON.stringify(saved.task))
420
+ throw new Error('ownership reset marker readback failed');
421
+ return { operation_id: source.operation_id, reset: true, created: true };
422
+ }
423
+ catch (error) {
424
+ if (!(error instanceof Error) || !/snapshot|revision conflict|already reset/.test(error.message))
425
+ throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
426
+ const winner = this.taskSource(source.operation_id);
427
+ if (winner) {
428
+ let parsed;
429
+ try {
430
+ parsed = JSON.parse(winner.source);
431
+ }
432
+ catch { }
433
+ if (parsed && typeof parsed === 'object' && Object.hasOwn(parsed, 'task_reset')) {
434
+ this.decode({ operation_id: winner.operation_id, revision: winner.revision, task: parsed });
435
+ return { operation_id: source.operation_id, reset: true, created: false };
436
+ }
437
+ }
438
+ throw ownershipFailure(error instanceof Error && /revision|snapshot|conflict/.test(error.message) ? 'conflict' : 'persistence_unavailable', error instanceof Error ? error.message : String(error));
439
+ }
440
+ }
441
+ isReset(record) { return Object.hasOwn(record, 'task_reset'); }
442
+ async resetUnusableTasks(subAid, ownerAid) {
443
+ for (const snapshot of this.list(true)) {
444
+ if (this.isReset(snapshot.task)) {
445
+ this.decode(snapshot);
446
+ continue;
447
+ }
448
+ if (snapshot.task.sub_aid !== subAid)
449
+ continue;
450
+ if (!this.isReset(snapshot.task) && ['completed', 'rejected', 'stopped'].includes(snapshot.task.status))
451
+ continue;
452
+ let record;
453
+ let unusable = false;
454
+ try {
455
+ record = this.decode(snapshot).record;
456
+ }
457
+ catch (error) {
458
+ if (this.isReset(snapshot.task) || !(error instanceof OwnershipTaskDecodeError) || error.operationId !== snapshot.operation_id)
459
+ throw error;
460
+ unusable = true;
461
+ record = snapshot.task;
462
+ }
463
+ if (!unusable && !this.isReset(record) && !record.base_agent_md && record.request_verified && !['completed', 'stopped', 'rejected'].includes(record.status) && record.operation === 'bind' && record.owner_aid === ownerAid && record.initiator_aid === this.local()) {
464
+ try {
465
+ this.legacyTaskView(record);
466
+ }
467
+ catch {
468
+ unusable = true;
469
+ }
470
+ }
471
+ if (!unusable)
472
+ continue;
473
+ const source = this.taskSource(snapshot.operation_id);
474
+ if (!source || source.revision !== snapshot.revision)
475
+ throw ownershipFailure('conflict', 'ownership task advanced before automatic reset');
476
+ let parsed;
477
+ try {
478
+ parsed = JSON.parse(source.source);
479
+ }
480
+ catch { }
481
+ if (!parsed || typeof parsed !== 'object' || parsed.sub_aid !== subAid)
482
+ throw ownershipFailure('persistence_unavailable', 'ownership task subject cannot be established for automatic reset');
483
+ if (Object.hasOwn(parsed, 'task_reset'))
484
+ throw ownershipFailure('persistence_unavailable', 'ownership reset marker is corrupt');
485
+ if (['completed', 'rejected', 'stopped'].includes(parsed.status))
486
+ continue;
487
+ await this.resetSource(source, subAid);
488
+ }
489
+ }
293
490
  decode(snapshot) {
294
491
  const record = snapshot.task;
295
492
  try {
493
+ if (this.isReset(record)) {
494
+ const task = snapshot.task, reset = task.task_reset;
495
+ if (task.protocol !== '2.1' || task.operation_id !== snapshot.operation_id || !OWNERSHIP_UUID_V4_RE.test(snapshot.operation_id) || task.local_aid !== this.local() || task.status !== 'stopped' || task.step !== 'reset' || Object.keys(task).some((key) => !['protocol', 'operation_id', 'local_aid', 'status', 'step', 'sub_aid', 'task_reset'].includes(key)) || !reset || typeof reset !== 'object' || Array.isArray(reset) || Object.keys(reset).length !== 4 || typeof reset.source !== 'string' || reset.source_sha256 !== ownershipSha256(reset.source) || !Number.isSafeInteger(reset.revision) || Number(reset.revision) < 1 || Number(reset.revision) + 1 !== snapshot.revision || !Number.isSafeInteger(reset.reset_at) || Number(reset.reset_at) <= 0)
496
+ throw new Error('ownership reset marker is corrupt');
497
+ let subject;
498
+ try {
499
+ subject = ownershipAid(JSON.parse(reset.source).sub_aid, 'sub_aid');
500
+ }
501
+ catch { }
502
+ if (task.sub_aid !== subject)
503
+ throw new Error('ownership reset subject changed');
504
+ return { record, revision: snapshot.revision };
505
+ }
296
506
  if (record.protocol !== '2.1' || typeof record.operation_id !== 'string' || !OWNERSHIP_UUID_V4_RE.test(record.operation_id) || record.operation_id !== snapshot.operation_id || !['bind', 'release', 'transfer'].includes(record.operation) || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(record.status) || typeof record.step !== 'string')
297
507
  throw new Error('invalid ownership task');
508
+ if (!record.base_agent_md && record.request_verified && !record.legacy_task)
509
+ return { record, revision: snapshot.revision };
298
510
  ownershipAid(record.sub_aid, 'sub_aid');
299
511
  ownershipAid(record.owner_aid, 'owner_aid');
300
512
  ownershipAid(record.initiator_aid, 'initiator_aid');
@@ -304,28 +516,82 @@ export class OwnershipWorkflow {
304
516
  }
305
517
  else if (record.new_owner_aid !== undefined)
306
518
  throw new Error('unexpected transfer target');
307
- if (ownershipInteger(record.target_relation_version, 'target_relation_version') !== ownershipInteger(record.base_relation_version, 'base_relation_version', 0) + (record.operation === 'release' ? 0 : 1) || !participants(record).includes(record.initiator_aid) || record.operation !== 'bind' && record.initiator_aid !== record.owner_aid)
519
+ const baseVersion = ownershipInteger(record.base_relation_version, 'base_relation_version', 0), targetVersion = ownershipInteger(record.target_relation_version, 'target_relation_version');
520
+ if (targetVersion !== baseVersion + 1 && !(record.operation === 'release' && targetVersion === baseVersion) || !participants(record).includes(record.initiator_aid) || record.operation !== 'bind' && record.initiator_aid !== record.owner_aid)
308
521
  throw new Error('invalid fixed ownership context');
309
522
  if (JSON.stringify(record.local_roles) !== JSON.stringify(localRoles(record, this.local())) || !record.local_roles.length || !['pending', 'accept', 'reject'].includes(record.local_decision) || !record.approvals || participants(record).some((aid) => !['pending', 'accept', 'reject'].includes(record.approvals[aid])) || record.approvals[this.local()] !== record.local_decision || !record.files || typeof record.files !== 'object' || Array.isArray(record.files) || !Array.isArray(record.inbox) || !Array.isArray(record.outbox) || !Array.isArray(record.received) || !Array.isArray(record.notices) || typeof record.request_verified !== 'boolean' || typeof record.request_notified !== 'boolean')
310
523
  throw new Error('ownership approval or pending state is corrupt');
311
524
  if (Object.keys(record.approvals).length !== participants(record).length || record.approvals[record.initiator_aid] !== 'accept' || record.initiator_aid === this.local() && record.local_decision !== 'accept')
312
525
  throw new Error('ownership initiator or approval participants are corrupt');
313
- if (record.request_verified) {
526
+ if (record.legacy_recovery !== undefined) {
527
+ const recovery = record.legacy_recovery, original = recovery.task, checkpoint = recovery.checkpoint;
528
+ if (!original || record.request_verified !== true || recovery.task_sha256 !== ownershipSha256(JSON.stringify(original)) || !Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || !checkpoint || !Number.isSafeInteger(checkpoint.observed_at) || checkpoint.observed_at <= 0 || typeof recovery.owner_confirmed !== 'boolean')
529
+ throw new Error('legacy recovery checkpoint is corrupt');
530
+ this.legacyView(original);
531
+ if (['operation_id', 'operation', 'sub_aid', 'owner_aid', 'initiator_aid', 'base_relation_version', 'target_relation_version'].some((key) => original[key] !== record[key]) || JSON.stringify(checkpoint.agent_md) !== JSON.stringify(record.base_agent_md) || original.local_decision !== 'pending' && original.local_decision !== record.local_decision || participants(record).some((name) => original.approvals[name] !== 'pending' && original.approvals[name] !== record.approvals[name]))
532
+ throw new Error('legacy recovery original context or approval changed');
533
+ const owner = checkpoint.owner_md;
534
+ if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
535
+ throw new Error('legacy recovery owner observation is corrupt');
536
+ if (owner.exists) {
537
+ const file = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
538
+ if (file.action !== 'declare' || file.statement?.relation_version !== 1)
539
+ throw new Error('legacy recovery owner observation changed');
540
+ }
541
+ const originalOwner = original.files.owner, pinned = record.files.owner;
542
+ if ((originalOwner || owner.exists) && (!pinned || pinned.raw !== (originalOwner?.raw ?? owner.raw) || pinned.hash !== (originalOwner?.hash ?? owner.sha256) || JSON.stringify(pinned.original_owner) !== JSON.stringify(owner)) || originalOwner && (!owner.exists || originalOwner.raw !== owner.raw || originalOwner.hash !== owner.sha256))
543
+ throw new Error('legacy recovery owner candidate changed');
544
+ if (this.local() === record.owner_aid ? !recovery.owner_confirmed : recovery.owner_confirmed ? !record.received.includes(`${record.owner_aid}|${record.operation_id}|owner.ready`) : Boolean(record.files.agent) || record.status === 'completed')
545
+ throw new Error('legacy recovery owner confirmation is corrupt');
546
+ }
547
+ if (record.legacy_task) {
548
+ const recovery = record.legacy_task, original = recovery.task;
549
+ if (!Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || recovery.task_sha256 !== ownershipSha256(JSON.stringify(original)))
550
+ throw new Error('legacy task source is corrupt');
551
+ this.priorView(original);
552
+ if (['operation_id', 'operation', 'sub_aid', 'owner_aid', 'new_owner_aid', 'initiator_aid', 'base_relation_version', 'target_relation_version'].some((key) => original[key] !== record[key]) || Object.entries(original.approvals).some(([aid, decision]) => decision !== 'pending' && record.approvals[aid] !== decision) || JSON.stringify(recovery.checkpoint.agent_md) !== JSON.stringify(record.base_agent_md ?? null) || JSON.stringify(recovery.checkpoint.owner_md) !== JSON.stringify(record.base_owner_md ?? null) || !record.base_agent_md && record.status !== 'completed')
553
+ throw new Error('legacy task fixed context changed');
554
+ for (const [key, saved] of Object.entries(original.files))
555
+ if (record.files[key]?.raw !== saved.raw || record.files[key]?.hash !== saved.hash || key === 'agent' && JSON.stringify(record.files[key]?.condition) !== JSON.stringify(saved.condition))
556
+ throw new Error('legacy task signed candidate changed');
557
+ }
558
+ if (record.request_verified && (record.base_agent_md || !record.legacy_task)) {
314
559
  const base = record.base_agent_md;
315
- if (!base || base.exists !== true || typeof base.raw !== 'string' || base.sha256 !== ownershipSha256(base.raw) || typeof base.etag !== 'string' || !base.etag)
560
+ if (!base || typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || base.sha256 !== ownershipSha256(base.raw) || typeof base.etag !== 'string' || (base.exists ? !base.etag : base.raw !== '' || base.etag !== '' || record.operation !== 'bind' || record.base_relation_version !== 0))
316
561
  throw new Error('ownership original agent.md evidence is corrupt');
317
- const statement = parseOwnershipFile(base.raw, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
318
- if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version)
562
+ const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
563
+ if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version && !(base.pending && statement?.relation_version === record.target_relation_version && statement.owner_aid === record.owner_aid) || base.pending && record.operation !== 'bind')
319
564
  throw new Error('ownership original statement is corrupt');
320
565
  const owner = record.base_owner_md;
321
566
  if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
322
567
  throw new Error('ownership original owner.md evidence is corrupt');
323
568
  if (owner.exists) {
324
569
  const declaration = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: statement?.owner_aid ?? record.owner_aid });
325
- if (declaration.statement?.relation_version !== record.base_relation_version)
570
+ if (!base.pending && declaration.statement?.relation_version !== record.base_relation_version)
326
571
  throw new Error('ownership original owner statement is corrupt');
327
572
  }
328
573
  }
574
+ if (record.pending_agent_md) {
575
+ const pending = record.pending_agent_md;
576
+ const statement = parseOwnershipFile(pending.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
577
+ if (record.operation !== 'bind' || pending.sha256 !== ownershipSha256(pending.raw) || !pending.etag || statement?.owner_aid !== record.owner_aid || statement.relation_version !== record.target_relation_version)
578
+ throw new Error('ownership pending original is corrupt');
579
+ }
580
+ if (record.owner_proposal) {
581
+ const proposal = record.owner_proposal, file = parseOwnershipFile(proposal.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
582
+ if (!this.ownerFirst(record) || proposal.sha256 !== ownershipSha256(proposal.raw) || file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version)
583
+ throw new Error('ownership owner proposal is corrupt');
584
+ }
585
+ if (record.owner_first !== undefined && (record.owner_first !== true || record.operation !== 'bind' || record.initiator_aid !== record.owner_aid || record.sub_aid === record.owner_aid) || record.owner_first && record.request_verified && this.local() === record.sub_aid && !record.owner_proposal)
586
+ throw new Error('ownership owner-first context is corrupt');
587
+ if (record.rejection_cleanup) {
588
+ const cleanup = record.rejection_cleanup;
589
+ if (record.operation !== 'bind' || record.status !== 'rejected' || !record.local_roles.includes('sub') || !record.files.agent || typeof cleanup.cleaned !== 'boolean' || !cleanup.files || Object.keys(cleanup.files).some((key) => key !== 'agent'))
590
+ throw new Error('ownership rejection cleanup is corrupt');
591
+ const file = cleanup.files.agent;
592
+ if (file && (file.hash !== ownershipSha256(file.raw) || file.condition.if_match !== record.files.agent.published_etag || Object.keys(file.condition).length !== 1 || parseOwnershipFile(file.raw, 'agent', { expectedSubAid: record.sub_aid }).statement))
593
+ throw new Error('ownership rejection cleanup original is corrupt');
594
+ }
329
595
  for (const item of record.inbox)
330
596
  parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record });
331
597
  for (const key of record.received) {
@@ -371,7 +637,7 @@ export class OwnershipWorkflow {
371
637
  if (record.status === 'completed') {
372
638
  const evidence = record.completion_evidence;
373
639
  const cleaned = record.operation === 'release' && evidence?.status === 'unbound' && evidence.relation_version === 0 && record.cleanup_status === 'cleaned' && Boolean(this.cleanupEvidence(record, evidence));
374
- if (!evidence || evidence.sub_aid !== record.sub_aid || !cleaned && (evidence.relation_version !== record.target_relation_version || evidence.status !== (record.operation === 'release' ? 'released' : 'consensus')) || evidence.lock !== (record.operation === 'release' ? 'unlocked' : 'locked') || evidence.owner_aid !== (record.operation === 'release' ? null : targetOwner(record)) || typeof evidence.agent_md_etag !== 'string' || !evidence.agent_md_etag || !/^[0-9a-f]{64}$/.test(evidence.agent_md_sha256) || typeof evidence.owner_md_sha256 !== 'string' || record.operation !== 'release' && !/^[0-9a-f]{64}$/.test(evidence.owner_md_sha256))
640
+ if (!evidence || evidence.sub_aid !== record.sub_aid || !cleaned && (evidence.relation_version !== (record.operation === 'release' ? record.base_relation_version : record.target_relation_version) || evidence.status !== (record.operation === 'release' ? 'released' : 'consensus')) || evidence.lock !== (record.operation === 'release' ? 'unlocked' : 'locked') || evidence.owner_aid !== (record.operation === 'release' ? null : targetOwner(record)) || typeof evidence.agent_md_etag !== 'string' || !evidence.agent_md_etag || !/^[0-9a-f]{64}$/.test(evidence.agent_md_sha256) || typeof evidence.owner_md_sha256 !== 'string' || record.operation !== 'release' && !/^[0-9a-f]{64}$/.test(evidence.owner_md_sha256))
375
641
  throw new Error('ownership target acceptance evidence is corrupt');
376
642
  if (record.operation !== 'release' && (record.local_roles.includes('sub') && record.files.agent?.hash !== evidence.agent_md_sha256 || this.local() === targetOwner(record) && record.files.owner?.hash !== evidence.owner_md_sha256 || record.operation === 'transfer' && record.local_roles.includes('owner') && !record.files.transfer))
377
643
  throw new Error('ownership target file evidence is missing');
@@ -381,8 +647,195 @@ export class OwnershipWorkflow {
381
647
  return { record, revision: snapshot.revision };
382
648
  }
383
649
  catch (error) {
384
- throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
650
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
651
+ throw error;
652
+ throw new OwnershipTaskDecodeError(snapshot.operation_id, error instanceof Error ? error.message : String(error));
653
+ }
654
+ }
655
+ legacyTaskView(record) {
656
+ return record.operation === 'bind' && record.base_relation_version === 0 && record.initiator_aid === record.sub_aid && Object.keys(record.files ?? {}).every((key) => key === 'owner') ? this.legacyView(record) : this.priorView(record);
657
+ }
658
+ legacyView(record) {
659
+ if (Object.hasOwn(record, 'legacy_recovery') || record.protocol !== '2.1' || record.operation !== 'bind' || record.base_relation_version !== 0 || record.target_relation_version !== 1 || record.initiator_aid !== record.sub_aid || record.request_verified !== true || record.status !== 'pending' && !(record.status === 'error' && record.retryable === true) || record.base_agent_md != null || record.base_owner_md != null || !record.files || typeof record.files !== 'object' || Array.isArray(record.files) || Object.keys(record.files).some((key) => key !== 'owner') || record.local_decision === 'reject' || Object.values(record.approvals ?? {}).includes('reject'))
660
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
661
+ const view = JSON.parse(JSON.stringify(record));
662
+ view.request_verified = false;
663
+ view.files = {};
664
+ view.inbox = record.inbox?.map((item) => ({ from: item.from, payload: this.legacyMessage(item.payload) }));
665
+ view.outbox = record.outbox?.map((item) => ({ to: item.to, payload: this.legacyMessage(item.payload) }));
666
+ this.decode({ operation_id: record.operation_id, revision: 1, task: view });
667
+ for (const notice of record.notices) {
668
+ if (typeof notice !== 'string' || notice.split('|').length !== 2)
669
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership notice is corrupt');
670
+ const [to, action] = notice.split('|');
671
+ this.validateOutgoing(view, { to: to, payload: { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: record.operation_id, operation: record.operation, action: action, sub_aid: record.sub_aid, owner_aid: record.owner_aid, base_relation_version: 0, target_relation_version: 1 } });
672
+ }
673
+ const saved = record.files.owner;
674
+ if (saved) {
675
+ if (this.local() !== record.owner_aid || record.local_decision !== 'accept' || typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !saved.condition || Object.keys(saved.condition).length !== 1 || typeof saved.condition.expected_revision !== 'string' || !saved.condition.expected_revision || saved.published_revision !== undefined && (typeof saved.published_revision !== 'string' || !saved.published_revision))
676
+ throw ownershipFailure('unsupported_protocol', 'legacy owner candidate cannot be recovered automatically');
677
+ const file = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
678
+ if (file.action !== 'declare' || file.statement?.relation_version !== 1)
679
+ throw ownershipFailure('unsupported_protocol', 'legacy owner candidate does not match this task');
680
+ }
681
+ return view;
682
+ }
683
+ legacyMessage(content) {
684
+ if (!content || typeof content !== 'object' || Array.isArray(content))
685
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership message is corrupt');
686
+ return { ...content };
687
+ }
688
+ async upgradeLegacy(handle) {
689
+ if (Boolean(handle.record.base_agent_md) || !handle.record.request_verified || ['completed', 'stopped', 'rejected'].includes(handle.record.status))
690
+ return;
691
+ if (handle.record.operation !== 'bind' || handle.record.base_relation_version !== 0 || handle.record.initiator_aid !== handle.record.sub_aid || Object.keys(handle.record.files ?? {}).some((key) => key !== 'owner'))
692
+ return this.upgradePrior(handle);
693
+ const original = JSON.parse(JSON.stringify(handle.record));
694
+ const record = this.legacyView(original);
695
+ for (const snapshot of this.list(true)) {
696
+ if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
697
+ continue;
698
+ const other = this.decode(snapshot).record;
699
+ if (!other.base_agent_md && other.request_verified && !['completed', 'stopped', 'rejected'].includes(other.status))
700
+ this.legacyTaskView(other);
701
+ if (other.status === 'pending' || other.status === 'error' && other.retryable)
702
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
703
+ }
704
+ const ns = await this.readNs(record.sub_aid);
705
+ if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
706
+ throw ownershipFailure('invalid', 'human cannot declare an owner');
707
+ if (ns.lock === 'locked' && ns.owner_aid === record.owner_aid && ns.relation_version === record.target_relation_version)
708
+ return this.upgradePrior(handle);
709
+ if (ns.lock !== 'unlocked' || ns.relation_version !== 0 || ns.owner_aid || ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement)
710
+ throw ownershipFailure('conflict', 'legacy ownership relation has changed');
711
+ const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
712
+ if (current.exists) {
713
+ const file = await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, record.owner_aid);
714
+ if (file.action !== 'declare' || file.statement?.relation_version !== 1)
715
+ throw ownershipFailure('conflict', 'legacy owner declaration has changed');
716
+ }
717
+ const candidate = original.files.owner;
718
+ if (candidate) {
719
+ await this.deps.files.verifyFile(candidate.raw, 'owner', record.sub_aid, record.owner_aid);
720
+ if (!current.exists || current.content !== candidate.raw || current.sha256 !== candidate.hash)
721
+ throw ownershipFailure('conflict', 'legacy owner candidate was replaced');
722
+ if (candidate.published_revision !== undefined && current.revision !== candidate.published_revision)
723
+ throw ownershipFailure(current.revision ? 'conflict' : 'unavailable', 'legacy published owner revision changed', !current.revision);
385
724
  }
725
+ else if (this.local() === record.owner_aid && current.exists)
726
+ throw ownershipFailure('unsupported_protocol', 'legacy owner declaration lacks a local task candidate');
727
+ const owner = { exists: current.exists, raw: current.content, sha256: current.exists ? ownershipSha256(current.content) : '', ...(candidate && current.revision ? { revision: current.revision } : {}) };
728
+ record.status = 'pending';
729
+ record.step = 'approval';
730
+ record.retryable = true;
731
+ record.request_verified = true;
732
+ delete record.code;
733
+ delete record.reason;
734
+ record.inbox = [];
735
+ record.received = [];
736
+ record.outbox = [];
737
+ record.notices = [];
738
+ await this.captureBase(record, ns);
739
+ if (candidate || current.exists && this.local() === record.sub_aid)
740
+ record.files.owner = { raw: candidate?.raw ?? current.content, hash: candidate?.hash ?? current.sha256, condition: {}, original_owner: owner };
741
+ record.legacy_recovery = { task: original, revision: handle.revision, task_sha256: ownershipSha256(JSON.stringify(original)), checkpoint: { observed_at: Math.trunc(Date.now() / 1000), agent_md: record.base_agent_md, owner_md: owner }, owner_confirmed: this.local() === record.owner_aid };
742
+ if (this.local() === record.sub_aid)
743
+ this.enqueue(record, record.owner_aid, 'bind.request');
744
+ else if (record.local_decision === 'accept')
745
+ this.enqueue(record, record.sub_aid, 'bind.accept');
746
+ const after = await this.readNs(record.sub_aid), afterOwner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
747
+ if (after.exists !== ns.exists || after.content !== ns.content || after.etag !== ns.etag || after.relation_version !== 0 || after.lock !== 'unlocked' || afterOwner.exists !== current.exists || afterOwner.content !== current.content || afterOwner.sha256 !== current.sha256)
748
+ throw ownershipFailure('conflict', 'legacy recovery observations changed');
749
+ const recovered = { record, revision: handle.revision };
750
+ this.decode({ operation_id: record.operation_id, revision: handle.revision + 1, task: record });
751
+ try {
752
+ await this.save(recovered);
753
+ }
754
+ catch (error) {
755
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable' && error.message.includes('active ownership task already exists'))
756
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
757
+ throw error;
758
+ }
759
+ handle.record = record;
760
+ handle.revision = recovered.revision;
761
+ }
762
+ priorView(original) {
763
+ if (original.base_agent_md != null || original.base_owner_md != null || original.legacy_task || original.legacy_recovery || !original.request_verified || !['pending', 'error'].includes(original.status) || original.status === 'error' && !original.retryable || Object.values(original.approvals ?? {}).includes('reject'))
764
+ throw ownershipFailure('unsupported_protocol', 'legacy task context cannot be recovered');
765
+ const record = structuredClone(original);
766
+ record.request_verified = false;
767
+ for (const [key, saved] of Object.entries(record.files ?? {})) {
768
+ const owner = key === 'owner' ? targetOwner(record) : record.owner_aid;
769
+ if (this.local() !== (key === 'agent' ? record.sub_aid : owner))
770
+ throw ownershipFailure('unsupported_protocol', 'legacy file local role is invalid');
771
+ if (key !== 'agent' && key !== 'cleanup') {
772
+ const condition = saved.condition;
773
+ if (!saved.original_owner && (!condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision))
774
+ throw ownershipFailure('unsupported_protocol', 'legacy owner condition is invalid');
775
+ saved.condition = {};
776
+ saved.original_owner ??= { exists: false, raw: '', sha256: '' };
777
+ }
778
+ }
779
+ try {
780
+ this.decode({ operation_id: original.operation_id, revision: 1, task: record });
781
+ }
782
+ catch (error) {
783
+ throw ownershipFailure('unsupported_protocol', error instanceof Error ? error.message : String(error));
784
+ }
785
+ return record;
786
+ }
787
+ async upgradePrior(handle) {
788
+ const original = structuredClone(handle.record), record = this.priorView(original);
789
+ for (const snapshot of this.list(true)) {
790
+ if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
791
+ continue;
792
+ const other = this.decode(snapshot).record;
793
+ if (other.status === 'pending' || other.status === 'error' && other.retryable)
794
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
795
+ }
796
+ const ns = await this.readNs(record.sub_aid);
797
+ if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
798
+ throw ownershipFailure('invalid', 'human cannot declare an owner');
799
+ const base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
800
+ const released = record.operation === 'release' && ns.lock === 'unlocked' && !ns.owner_aid && ns.relation_version === record.base_relation_version;
801
+ const committed = record.operation !== 'release' && ns.lock === 'locked' && ns.owner_aid === targetOwner(record) && ns.relation_version === record.target_relation_version;
802
+ const pending = record.operation === 'bind' && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.target_relation_version && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement?.owner_aid === record.owner_aid;
803
+ if (!base && !released && !committed && !pending)
804
+ throw ownershipFailure('conflict', 'legacy relationship no longer matches this task');
805
+ if (!committed)
806
+ await this.captureBase(record, ns);
807
+ for (const [key, saved] of Object.entries(record.files)) {
808
+ const owner = key === 'owner' || key === 'agent' ? targetOwner(record) : record.owner_aid;
809
+ await this.deps.files.verifyFile(saved.raw, key === 'agent' ? 'agent' : 'owner', record.sub_aid, owner);
810
+ if (key === 'agent')
811
+ continue;
812
+ const current = await this.deps.files.readOwner(owner, record.sub_aid);
813
+ const source = original.files[key];
814
+ const revision = source.condition.expected_revision;
815
+ if (revision && (current.content === saved.raw && source.published_revision !== undefined || !committed && current.content !== saved.raw)) {
816
+ if (!current.revision)
817
+ throw ownershipFailure('unavailable', 'legacy owner original revision is unavailable', true);
818
+ if (current.revision !== (current.content === saved.raw ? source.published_revision : revision))
819
+ throw ownershipFailure('conflict', 'legacy owner original revision changed');
820
+ }
821
+ const currentStatement = current.exists ? parseOwnershipFile(current.content, 'owner', { expectedSubAid: record.sub_aid }).statement : undefined;
822
+ const expected = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid }).statement;
823
+ const same = expected && currentStatement && Object.keys(expected).every((field) => expected[field] === currentStatement[field]);
824
+ if (current.content !== saved.raw && (source.published_revision !== undefined || current.exists && (key === 'owner' || !same)) && !committed)
825
+ throw ownershipFailure('conflict', 'legacy signed owner file was replaced');
826
+ saved.original_owner = { exists: current.exists, raw: current.content, sha256: current.exists ? current.sha256 : '', ...(revision && current.revision ? { revision: current.revision } : {}) };
827
+ }
828
+ record.request_verified = true;
829
+ record.legacy_task = { task: original, revision: handle.revision, task_sha256: ownershipSha256(JSON.stringify(original)), checkpoint: { agent_md: record.base_agent_md ?? null, owner_md: record.base_owner_md ?? null } };
830
+ const recovered = { record, revision: handle.revision };
831
+ if (committed) {
832
+ if (!await this.confirm(recovered))
833
+ throw ownershipFailure('conflict', 'legacy committed target lacks matching local signed evidence');
834
+ }
835
+ else
836
+ await this.save(recovered);
837
+ handle.record = record;
838
+ handle.revision = recovered.revision;
386
839
  }
387
840
  async save(handle) {
388
841
  try {
@@ -448,23 +901,44 @@ export class OwnershipWorkflow {
448
901
  };
449
902
  if (context.operation === 'transfer')
450
903
  record.new_owner_aid = context.new_owner_aid;
904
+ if (context.operation === 'bind' && initiator === context.owner_aid && context.owner_aid !== context.sub_aid)
905
+ record.owner_first = true;
451
906
  record.approvals[this.local()] = decision;
452
907
  return record;
453
908
  }
454
909
  async start(operation, subAid, ownerAid, newOwnerAid, operationId) {
455
910
  this.requireApplicable(subAid);
456
911
  return this.runSerialized(subAid, async () => {
912
+ if (operation === 'bind' && !operationId)
913
+ await this.resetUnusableTasks(subAid, ownerAid);
457
914
  const existing = operationId ? this.load(operationId) : null;
458
915
  if (existing) {
916
+ if (this.isReset(existing.record))
917
+ throw ownershipFailure('conflict', 'ownership task was reset');
918
+ await this.upgradeLegacy(existing);
459
919
  const record = existing.record;
920
+ if (!record.base_agent_md && record.request_verified)
921
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
460
922
  if (record.operation !== operation || record.sub_aid !== subAid || record.owner_aid !== ownerAid || record.new_owner_aid !== newOwnerAid || record.initiator_aid !== this.local())
461
923
  throw ownershipFailure('conflict', 'saved registration task context changed');
462
924
  await this.drive(existing);
463
925
  return ownershipTaskResult(record);
464
926
  }
465
927
  let ns;
466
- for (const snapshot of this.list()) {
467
- const active = this.decode(snapshot), record = active.record;
928
+ for (const snapshot of this.list(true)) {
929
+ if (snapshot.task.sub_aid !== subAid)
930
+ continue;
931
+ const active = this.decode(snapshot);
932
+ if (this.isReset(active.record))
933
+ continue;
934
+ if (!active.record.base_agent_md && active.record.request_verified && ['completed', 'stopped', 'rejected'].includes(active.record.status))
935
+ continue;
936
+ if (!active.record.base_agent_md && active.record.request_verified && (active.record.operation !== operation || active.record.owner_aid !== ownerAid || active.record.new_owner_aid !== newOwnerAid || active.record.initiator_aid !== this.local()))
937
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
938
+ await this.upgradeLegacy(active);
939
+ const record = active.record;
940
+ if (!record.base_agent_md && record.request_verified)
941
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
468
942
  if (record.sub_aid !== subAid || record.status !== 'pending' && !(record.status === 'error' && record.retryable))
469
943
  continue;
470
944
  const same = record.operation === operation && record.owner_aid === ownerAid && record.new_owner_aid === newOwnerAid && record.initiator_aid === this.local();
@@ -477,7 +951,7 @@ export class OwnershipWorkflow {
477
951
  await this.recordError(active, error);
478
952
  return ownershipTaskResult(record);
479
953
  }
480
- let base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
954
+ let base = this.isBase(record, ns);
481
955
  if (base && record.request_verified) {
482
956
  try {
483
957
  await this.assertBaseMaterial(active, ns);
@@ -488,7 +962,7 @@ export class OwnershipWorkflow {
488
962
  base = false;
489
963
  }
490
964
  }
491
- const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.target_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
965
+ const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.base_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
492
966
  if (!base && !target) {
493
967
  await this.recordError(active, ownershipFailure('conflict', 'the operation was superseded by a different relation result'));
494
968
  const result = ownershipTaskResult(record);
@@ -508,11 +982,13 @@ export class OwnershipWorkflow {
508
982
  ns = await this.readNs(subAid);
509
983
  if (operation === 'bind' && ns.lock !== 'unlocked')
510
984
  throw ownershipFailure('owner_locked', 'the current owner relation is locked');
511
- if (!ns.exists)
512
- throw ownershipFailure('unavailable', 'initial agent.md is not published', true);
513
- if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type === 'human')
985
+ if (operation === 'bind' && subAid !== this.local() && (ns.pending || ns.ownership_status === 'pending'))
986
+ throw ownershipFailure('conflict', 'pending ownership must resume its original operation');
987
+ if (!ns.exists && operation !== 'bind')
988
+ throw ownershipFailure('conflict', 'original agent.md is missing');
989
+ if (operation === 'bind' && (ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type : subAid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
514
990
  throw ownershipFailure('invalid', 'human cannot declare an owner');
515
- if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
991
+ if (operation === 'bind' && ns.exists && !(ns.pending || ns.ownership_status === 'pending') && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
516
992
  throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
517
993
  if (operation !== 'bind') {
518
994
  const consensus = await this.deps.files.checkConsensus(subAid);
@@ -536,14 +1012,21 @@ export class OwnershipWorkflow {
536
1012
  const initial = this.load(operationId);
537
1013
  if (!initial)
538
1014
  throw ownershipFailure('operation_not_found', 'no local ownership request matches operation_id');
1015
+ if (this.isReset(initial.record))
1016
+ throw ownershipFailure('conflict', 'ownership task was reset');
539
1017
  return this.runSerialized(initial.record.sub_aid, async () => {
540
1018
  const handle = this.load(operationId);
1019
+ await this.upgradeLegacy(handle);
541
1020
  const record = handle.record;
1021
+ if (!record.base_agent_md && record.request_verified)
1022
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
542
1023
  if (record.operation === 'release' || record.initiator_aid === this.local() || !record.request_verified)
543
1024
  throw ownershipFailure('forbidden', 'this task is not a verified local approval request');
544
1025
  if (record.local_decision !== 'pending') {
545
1026
  if (record.local_decision !== decision)
546
1027
  throw ownershipFailure('conflict', 'the persisted local approval cannot be changed');
1028
+ if (decision === 'accept' && record.status === 'stopped' && record.operation === 'transfer' && this.local() === record.sub_aid && record.step === 'agent' && record.code === 'conflict' && record.reason === 'conflict: target owner declaration does not match this task' && !await this.recoverTargetDeclaration(handle))
1029
+ return ownershipTaskResult(record);
547
1030
  await this.drive(handle);
548
1031
  return ownershipTaskResult(record);
549
1032
  }
@@ -552,12 +1035,13 @@ export class OwnershipWorkflow {
552
1035
  if (record.status !== 'pending')
553
1036
  throw ownershipFailure('conflict', 'ownership approval stage has ended');
554
1037
  const ns = await this.readNs(record.sub_aid);
555
- if (ns.relation_version !== record.base_relation_version || (record.operation === 'bind' ? ns.lock !== 'unlocked' : ns.lock !== 'locked' || ns.owner_aid !== record.owner_aid))
1038
+ if (!this.isBase(record, ns))
556
1039
  throw ownershipFailure('conflict', 'the original relation changed before approval');
557
1040
  await this.assertBaseMaterial(handle, ns);
558
1041
  record.local_decision = decision;
559
1042
  record.approvals[this.local()] = decision;
560
- this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
1043
+ if (decision !== 'accept' || !this.ownerFirst(record))
1044
+ this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
561
1045
  if (decision === 'reject') {
562
1046
  record.status = 'rejected';
563
1047
  record.step = 'approval';
@@ -571,6 +1055,42 @@ export class OwnershipWorkflow {
571
1055
  return ownershipTaskResult(record);
572
1056
  });
573
1057
  }
1058
+ async recoverTargetDeclaration(handle) {
1059
+ const record = handle.record, local = this.local(), base = record.base_agent_md;
1060
+ if (!base || record.new_owner_aid === local || record.files.agent || record.files.owner || record.owner_proposal || record.legacy_task || record.legacy_recovery || record.approvals[local] !== 'accept' || record.authorization_sha256 === undefined || record.expires_at === undefined)
1061
+ return false;
1062
+ const began = Date.now();
1063
+ const sameBase = (ns) => this.isBase(record, ns) && ns.exists === base.exists && ns.content === base.raw && ownershipSha256(ns.content) === base.sha256 && ns.etag === base.etag;
1064
+ try {
1065
+ if (!sameBase(await this.readNs(record.sub_aid)))
1066
+ return false;
1067
+ await this.deps.files.requireTransfer(this.fileTask(handle));
1068
+ const target = await this.deps.files.readOwner(targetOwner(record), record.sub_aid);
1069
+ if (!target.exists)
1070
+ return false;
1071
+ const declaration = await this.deps.files.verifyFile(target.content, 'owner', record.sub_aid, targetOwner(record));
1072
+ if (declaration.action !== 'declare' || declaration.statement.relation_version !== record.target_relation_version)
1073
+ return false;
1074
+ if (!sameBase(await this.readNs(record.sub_aid)))
1075
+ return false;
1076
+ await this.deps.files.requireTransfer(this.fileTask(handle));
1077
+ const after = await this.deps.files.readOwner(targetOwner(record), record.sub_aid);
1078
+ if (!after.exists || after.content !== target.content || after.sha256 !== target.sha256 || after.version !== target.version || after.revision !== target.revision || Date.now() - began > 30_000 || this.local() !== local)
1079
+ return false;
1080
+ this.assertActive(handle);
1081
+ }
1082
+ catch (error) {
1083
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
1084
+ throw error;
1085
+ return false;
1086
+ }
1087
+ record.status = 'pending';
1088
+ record.retryable = true;
1089
+ delete record.code;
1090
+ delete record.reason;
1091
+ await this.save(handle);
1092
+ return true;
1093
+ }
574
1094
  enqueue(record, to, action, reason) {
575
1095
  if (to === this.local())
576
1096
  return;
@@ -583,11 +1103,13 @@ export class OwnershipWorkflow {
583
1103
  payload.new_owner_aid = record.new_owner_aid;
584
1104
  if (reason !== undefined)
585
1105
  payload.reason = reason;
586
- record.outbox.push({ to, payload });
1106
+ record.outbox.push({ to, payload, message_id: `m-${randomUUID().replace(/-/g, '')}` });
587
1107
  record.notices.push(key);
588
1108
  }
589
1109
  validateOutgoing(record, item) {
590
1110
  try {
1111
+ if (item.message_id !== undefined && (typeof item.message_id !== 'string' || !/^m-[0-9a-f]{32}$/.test(item.message_id)))
1112
+ throw new Error('ownership outgoing message_id is corrupt');
591
1113
  const message = parseOwnershipMessage(item.payload, { authenticated_from_aid: this.local(), local_aid: item.to, task: { ...record, local_decision: 'accept' } });
592
1114
  const rejected = ['bind.reject', 'transfer.reject'].includes(message.action);
593
1115
  if (message.action !== 'operation.stopped' && record.local_decision !== (rejected ? 'reject' : 'accept') || record.approvals[this.local()] !== record.local_decision)
@@ -598,7 +1120,7 @@ export class OwnershipWorkflow {
598
1120
  throw new Error('ownership outgoing action lacks participant approvals');
599
1121
  }
600
1122
  catch (error) {
601
- throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
1123
+ throw new OwnershipTaskDecodeError(record.operation_id, error instanceof Error ? error.message : String(error));
602
1124
  }
603
1125
  }
604
1126
  async flush(handle) {
@@ -612,10 +1134,10 @@ export class OwnershipWorkflow {
612
1134
  continue;
613
1135
  }
614
1136
  const ns = await this.readNs(record.sub_aid);
615
- const base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
616
- const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.target_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
1137
+ const base = this.isBase(record, ns);
1138
+ const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.base_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
617
1139
  const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
618
- if (requiresTarget ? !target : !base) {
1140
+ if (requiresTarget ? !target : !base && !(record.operation === 'bind' && target && ['owner.ready', 'bind.accept'].includes(message.payload.action))) {
619
1141
  record.outbox.shift();
620
1142
  await this.save(handle);
621
1143
  continue;
@@ -629,7 +1151,12 @@ export class OwnershipWorkflow {
629
1151
  if (expiresAt <= Math.trunc(Date.now() / 1000))
630
1152
  throw ownershipFailure('conflict', 'transfer authorization expired');
631
1153
  }
632
- await this.deps.send(message.to, message.payload);
1154
+ if (!message.message_id) {
1155
+ message.message_id = `m-${randomUUID().replace(/-/g, '')}`;
1156
+ await this.save(handle);
1157
+ }
1158
+ this.assertActive(handle);
1159
+ await this.deps.send(message.to, message.payload, message.message_id);
633
1160
  handle.record.outbox.shift();
634
1161
  await this.save(handle);
635
1162
  }
@@ -642,11 +1169,35 @@ export class OwnershipWorkflow {
642
1169
  const currentOwner = ns.lock === 'locked' ? file?.statement?.owner_aid : null;
643
1170
  if (ns.lock === 'locked' && (!file?.statement || file.statement.relation_version !== ns.relation_version) || ns.owner_aid !== undefined && ns.owner_aid !== currentOwner)
644
1171
  throw ownershipFailure('unavailable', 'NS owner metadata and signed agent.md do not agree', true);
645
- return { ...ns, owner_aid: currentOwner };
1172
+ if (!file?.statement)
1173
+ return { ...ns, owner_aid: currentOwner };
1174
+ const consensus = await this.deps.files.checkConsensus(subAid);
1175
+ if (consensus.status === 'invalid' || consensus.status === 'unavailable' || consensus.status === 'not_applicable')
1176
+ throw ownershipFailure(consensus.code ?? consensus.status, consensus.reason ?? 'ownership materials cannot be verified', consensus.retryable);
1177
+ if (consensus.relation_version !== ns.relation_version || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== ownershipSha256(ns.content))
1178
+ throw ownershipFailure('unavailable', 'agent.md changed during ownership verification', true);
1179
+ return { ...ns, lock: consensus.lock, owner_aid: consensus.owner_aid };
1180
+ }
1181
+ isPending(record, ns) {
1182
+ if (record.operation !== 'bind' || ns.lock !== 'unlocked' || !(ns.pending || ns.ownership_status === 'pending') || ns.relation_version !== record.target_relation_version)
1183
+ return false;
1184
+ const saved = record.files.agent;
1185
+ const candidate = saved ? { raw: saved.raw, sha256: saved.hash, etag: saved.published_etag } : record.pending_agent_md ?? (record.base_agent_md?.pending ? record.base_agent_md : undefined);
1186
+ return Boolean(candidate && ns.content === candidate.raw && ownershipSha256(ns.content) === candidate.sha256 && (!candidate.etag || ns.etag === candidate.etag));
1187
+ }
1188
+ isPendingBase(record, ns) {
1189
+ const base = record.base_agent_md;
1190
+ return record.operation === 'bind' && Boolean(base?.pending) && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.base_relation_version && ns.content === base.raw && ns.etag === base.etag && ownershipSha256(ns.content) === base.sha256;
1191
+ }
1192
+ isBase(record, ns) {
1193
+ return this.isPending(record, ns) || this.isPendingBase(record, ns) || ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
646
1194
  }
647
1195
  async captureBase(record, ns) {
648
- const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
649
- record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement };
1196
+ const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
1197
+ const pending = record.operation === 'bind' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.target_relation_version && statement?.owner_aid === record.owner_aid;
1198
+ record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement, ...(record.operation === 'bind' && (ns.pending || ns.ownership_status === 'pending') ? { pending: true } : {}) };
1199
+ if (pending)
1200
+ record.pending_agent_md = { raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag };
650
1201
  if (!statement) {
651
1202
  record.base_owner_md = { exists: false, raw: '', sha256: '' };
652
1203
  return;
@@ -661,6 +1212,12 @@ export class OwnershipWorkflow {
661
1212
  async assertBaseMaterial(handle, ns) {
662
1213
  const record = handle.record;
663
1214
  this.assertAgentBaseMaterial(record, ns);
1215
+ if (record.owner_proposal) {
1216
+ const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
1217
+ if (!current.exists || current.content !== record.owner_proposal.raw || current.sha256 !== record.owner_proposal.sha256)
1218
+ throw ownershipFailure('conflict', 'owner binding proposal changed');
1219
+ await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, record.owner_aid);
1220
+ }
664
1221
  const original = record.base_owner_md;
665
1222
  if (!original)
666
1223
  throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is missing');
@@ -692,10 +1249,14 @@ export class OwnershipWorkflow {
692
1249
  throw ownershipFailure('conflict', 'the original owner statement was replaced');
693
1250
  }
694
1251
  assertAgentBaseMaterial(record, ns) {
1252
+ if (this.isPending(record, ns))
1253
+ return;
695
1254
  const base = record.base_agent_md;
696
- if (!base)
1255
+ if (!base || typeof base.raw !== 'string')
697
1256
  throw ownershipFailure('persistence_unavailable', 'original agent.md evidence is missing');
698
- const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
1257
+ if (base.pending && (ns.content !== base.raw || ns.etag !== base.etag))
1258
+ throw ownershipFailure('conflict', 'the pending ownership original was replaced');
1259
+ const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
699
1260
  const same = base.statement ? Boolean(statement && Object.keys(base.statement).every((key) => base.statement[key] === statement[key])) : statement === null && ns.content === base.raw && ownershipSha256(ns.content) === base.sha256;
700
1261
  if (ns.exists !== base.exists || !same)
701
1262
  throw ownershipFailure('conflict', 'the original ownership statement was replaced');
@@ -705,6 +1266,10 @@ export class OwnershipWorkflow {
705
1266
  while (record.inbox.length) {
706
1267
  const item = record.inbox[0];
707
1268
  const ns = await this.readNs(record.sub_aid);
1269
+ if (record.operation === 'bind' && record.request_verified && !record.pending_agent_md && item.payload.action === 'bind.accept' && item.from === record.sub_aid && (ns.pending || ns.ownership_status === 'pending') && ns.lock === 'unlocked' && ns.relation_version === record.target_relation_version && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement?.owner_aid === record.owner_aid) {
1270
+ record.pending_agent_md = { raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag };
1271
+ await this.save(handle);
1272
+ }
708
1273
  if (record.request_verified && ns.relation_version === record.base_relation_version && !['relation.confirmed', 'sub.cleaned'].includes(item.payload.action))
709
1274
  await this.assertBaseMaterial(handle, ns);
710
1275
  try {
@@ -723,7 +1288,29 @@ export class OwnershipWorkflow {
723
1288
  continue;
724
1289
  }
725
1290
  const action = item.payload.action;
1291
+ if (action === 'owner.ready' && record.legacy_recovery && record.status === 'pending') {
1292
+ const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
1293
+ if (!owner.exists)
1294
+ throw ownershipFailure(record.files.owner ? 'conflict' : 'unavailable', 'legacy owner confirmation declaration is missing', !record.files.owner);
1295
+ const file = await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
1296
+ if (file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version || record.files.owner && (record.files.owner.raw !== owner.content || record.files.owner.hash !== owner.sha256))
1297
+ throw ownershipFailure('conflict', 'legacy owner confirmation declaration changed');
1298
+ if (!record.files.owner)
1299
+ record.files.owner = { raw: owner.content, hash: owner.sha256, condition: {}, original_owner: { exists: true, raw: owner.content, sha256: owner.sha256 } };
1300
+ record.legacy_recovery.owner_confirmed = true;
1301
+ }
726
1302
  if (['bind.request', 'transfer.request'].includes(action)) {
1303
+ if (this.ownerFirst(record)) {
1304
+ if (ns.lock !== 'unlocked' || ns.pending || ns.ownership_status === 'pending')
1305
+ throw ownershipFailure('conflict', 'owner binding requires the original unlocked relation');
1306
+ const proposal = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
1307
+ if (!proposal.exists)
1308
+ throw ownershipFailure('unavailable', 'owner binding proposal is missing', true);
1309
+ const file = await this.deps.files.verifyFile(proposal.content, 'owner', record.sub_aid, record.owner_aid);
1310
+ if (file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version || record.owner_proposal && (record.owner_proposal.raw !== proposal.content || record.owner_proposal.sha256 !== proposal.sha256))
1311
+ throw ownershipFailure('conflict', 'owner binding proposal does not match this operation');
1312
+ record.owner_proposal = { raw: proposal.content, sha256: proposal.sha256 };
1313
+ }
727
1314
  if (!record.request_verified)
728
1315
  await this.captureBase(record, ns);
729
1316
  record.request_verified = true;
@@ -842,8 +1429,10 @@ export class OwnershipWorkflow {
842
1429
  fileTask(handle) {
843
1430
  const record = handle.record;
844
1431
  const task = { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid,
1432
+ stage: record.operation === 'bind' && record.initiator_aid === record.sub_aid && !record.legacy_recovery && !record.legacy_task,
845
1433
  new_owner_aid: record.new_owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version,
846
- files: record.files, authorization_sha256: record.authorization_sha256, expires_at: record.expires_at, base_agent_md: record.base_agent_md, base_owner_md: record.base_owner_md,
1434
+ files: record.files, authorization_sha256: record.authorization_sha256, expires_at: record.expires_at, base_agent_md: record.base_agent_md, base_owner_md: record.base_owner_md, pending_agent_md: record.pending_agent_md, owner_proposal: record.owner_proposal,
1435
+ assertActive: () => this.assertActive(handle),
847
1436
  persist: async () => {
848
1437
  record.authorization_sha256 = task.authorization_sha256;
849
1438
  record.expires_at = task.expires_at;
@@ -853,15 +1442,55 @@ export class OwnershipWorkflow {
853
1442
  } };
854
1443
  return task;
855
1444
  }
1445
+ assertActive(handle) {
1446
+ const current = this.load(handle.record.operation_id);
1447
+ if (!current || this.isReset(current.record) || current.revision !== handle.revision)
1448
+ throw ownershipFailure('conflict', 'ownership task changed before external action');
1449
+ }
1450
+ ownerFirst(record) {
1451
+ return record.operation === 'bind' && record.initiator_aid === record.owner_aid && record.owner_aid !== record.sub_aid && Boolean(record.owner_first || record.owner_proposal) && !record.legacy_recovery && !record.legacy_task;
1452
+ }
856
1453
  async drive(handle) {
1454
+ if (this.isReset(handle.record))
1455
+ return;
1456
+ await this.upgradeLegacy(handle);
857
1457
  const record = handle.record;
1458
+ if (!record.base_agent_md && record.request_verified)
1459
+ return;
858
1460
  if (record.status === 'error' && record.retryable) {
859
1461
  record.status = 'pending';
860
1462
  await this.save(handle);
861
1463
  }
862
1464
  try {
863
1465
  await this.processInbox(handle);
1466
+ if (this.ownerFirst(record) && record.status === 'pending' && record.request_verified && this.local() === record.owner_aid) {
1467
+ const ns = await this.readNs(record.sub_aid);
1468
+ if (this.isBase(record, ns)) {
1469
+ await this.assertBaseMaterial(handle, ns);
1470
+ record.step = 'owner';
1471
+ await this.save(handle);
1472
+ await this.deps.files.declareSubaid(this.fileTask(handle));
1473
+ }
1474
+ }
1475
+ if (record.operation === 'bind' && (record.initiator_aid === record.sub_aid || this.ownerFirst(record)) && record.status === 'pending' && record.request_verified && record.local_decision === 'accept' && this.local() === record.sub_aid && !record.legacy_recovery && !record.legacy_task) {
1476
+ record.step = 'agent';
1477
+ await this.save(handle);
1478
+ await this.deps.files.declareOwneraid(this.fileTask(handle));
1479
+ }
864
1480
  await this.flush(handle);
1481
+ if (this.ownerFirst(record) && record.status === 'rejected' && this.local() === record.owner_aid && record.files.owner)
1482
+ await this.deps.files.cancelOwnerProposal(this.fileTask(handle));
1483
+ if (record.operation === 'bind' && record.status === 'rejected' && this.local() === record.sub_aid && record.files.agent && !record.rejection_cleanup?.cleaned) {
1484
+ if (!record.rejection_cleanup) {
1485
+ record.rejection_cleanup = { files: {}, cleaned: false };
1486
+ await this.save(handle);
1487
+ }
1488
+ const task = this.fileTask(handle);
1489
+ task.files = record.rejection_cleanup.files;
1490
+ await this.deps.files.cancelPendingBind(task, record.files.agent);
1491
+ record.rejection_cleanup.cleaned = true;
1492
+ await this.save(handle);
1493
+ }
865
1494
  if (record.status === 'rejected' || record.status === 'stopped' || record.status === 'error')
866
1495
  return;
867
1496
  if (!record.request_verified)
@@ -898,7 +1527,7 @@ export class OwnershipWorkflow {
898
1527
  return;
899
1528
  }
900
1529
  const ns = await this.readNs(record.sub_aid);
901
- if (ns.relation_version !== record.base_relation_version || record.operation === 'bind' && ns.lock !== 'unlocked' || record.operation === 'transfer' && (ns.lock !== 'locked' || ns.owner_aid !== record.owner_aid)) {
1530
+ if (!this.isBase(record, ns)) {
902
1531
  throw ownershipFailure('conflict', 'the original relation was replaced');
903
1532
  }
904
1533
  await this.assertBaseMaterial(handle, ns);
@@ -907,6 +1536,8 @@ export class OwnershipWorkflow {
907
1536
  const approved = participants(record).every((name) => record.approvals[name] === 'accept');
908
1537
  if (record.operation === 'bind' && !approved || record.operation === 'transfer' && this.local() === record.owner_aid && !approved)
909
1538
  return;
1539
+ if (record.legacy_recovery && this.local() === record.sub_aid && !record.legacy_recovery.owner_confirmed)
1540
+ return;
910
1541
  delete record.code;
911
1542
  delete record.reason;
912
1543
  record.retryable = true;
@@ -932,11 +1563,17 @@ export class OwnershipWorkflow {
932
1563
  record.step = 'owner';
933
1564
  await this.save(handle);
934
1565
  await this.deps.files.declareSubaid(this.fileTask(handle));
935
- this.enqueue(record, record.sub_aid, 'owner.ready');
1566
+ if (!this.ownerFirst(record))
1567
+ this.enqueue(record, record.sub_aid, 'owner.ready');
936
1568
  await this.save(handle);
937
1569
  await this.flush(handle);
938
1570
  }
939
1571
  if (this.local() === record.sub_aid) {
1572
+ if (record.legacy_recovery && record.files.owner) {
1573
+ const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid), pinned = record.files.owner;
1574
+ if (!current.exists || current.content !== pinned.raw || current.sha256 !== pinned.hash)
1575
+ throw ownershipFailure('conflict', 'legacy owner declaration changed before subject publication');
1576
+ }
940
1577
  record.step = 'agent';
941
1578
  await this.save(handle);
942
1579
  await this.deps.files.declareOwneraid(this.fileTask(handle));
@@ -987,7 +1624,7 @@ export class OwnershipWorkflow {
987
1624
  record.cleanup_status = 'cleaned';
988
1625
  cleaned = true;
989
1626
  }
990
- const reached = cleaned || result.relation_version === record.target_relation_version && (record.operation === 'release' ? result.status === 'released' && result.lock === 'unlocked' : result.status === 'consensus' && result.owner_aid === targetOwner(record));
1627
+ const reached = cleaned || result.relation_version === (record.operation === 'release' ? record.base_relation_version : record.target_relation_version) && (record.operation === 'release' ? result.status === 'released' && result.lock === 'unlocked' : result.status === 'consensus' && result.owner_aid === targetOwner(record));
991
1628
  if (!reached)
992
1629
  return false;
993
1630
  if (record.operation === 'release' && !cleaned) {
@@ -1027,6 +1664,10 @@ export class OwnershipWorkflow {
1027
1664
  }
1028
1665
  if (record.operation !== 'release' && (record.local_decision !== 'accept' || this.local() === record.sub_aid && (!record.files.agent || record.files.agent.hash !== result.agent_md_sha256) || this.local() === targetOwner(record) && (!record.files.owner || record.files.owner.hash !== result.owner_md_sha256)))
1029
1666
  return false;
1667
+ if (record.owner_proposal && record.owner_proposal.sha256 !== result.owner_md_sha256)
1668
+ throw ownershipFailure('conflict', 'owner binding proposal changed before acceptance');
1669
+ if (record.operation === 'bind' && record.pending_agent_md && (record.pending_agent_md.sha256 !== result.agent_md_sha256 || record.pending_agent_md.etag !== result.agent_md_etag))
1670
+ throw ownershipFailure('conflict', 'pending ownership original changed before acceptance');
1030
1671
  if (record.status !== 'completed' && record.operation !== 'release' && this.local() === record.sub_aid && record.files.agent?.published_etag !== undefined && record.files.agent.published_etag !== result.agent_md_etag)
1031
1672
  throw ownershipFailure('conflict', 'published agent.md ETag changed');
1032
1673
  if (record.status !== 'completed') {
@@ -1106,7 +1747,7 @@ export class OwnershipWorkflow {
1106
1747
  }
1107
1748
  await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: record.sub_aid, owner_aid: targetOwner(record),
1108
1749
  base_relation_version: record.target_relation_version, target_relation_version: observed.relation_version,
1109
- files: record.release_cleanup.files, expected_agent_statement: original, persist: () => this.save(handle) });
1750
+ files: record.release_cleanup.files, expected_agent_statement: original, assertActive: () => this.assertActive(handle), persist: () => this.save(handle) });
1110
1751
  record.release_cleanup.cleaned = true;
1111
1752
  await this.save(handle);
1112
1753
  this.emit(record, 'ownership.progress');
@@ -1169,8 +1810,8 @@ export class OwnershipWorkflow {
1169
1810
  const payload = { ...ownershipTaskResult(record), local_roles: [...record.local_roles], ...extra };
1170
1811
  if (event === 'ownership.request')
1171
1812
  payload.requester_aid = record.initiator_aid;
1172
- if (record.verified_relation_version !== undefined)
1173
- payload.relation_version = record.verified_relation_version;
1813
+ if (record.completion_evidence !== undefined)
1814
+ payload.relation_version = record.completion_evidence.relation_version;
1174
1815
  queueMicrotask(() => { try {
1175
1816
  const pending = this.deps.emit(event, payload);
1176
1817
  if (pending)