@agentunion/fastaun 0.5.20 → 0.5.21

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 (107) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  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/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. 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
  14. 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
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  25. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  29. 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 +106 -43
  30. 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 +180 -9
  31. 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
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. 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 +1 -1
  34. 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 +2 -2
  35. package/dist/agent-md-schema.js +6 -4
  36. package/dist/agent-md-schema.js.map +1 -1
  37. package/dist/agent-md.d.ts +22 -2
  38. package/dist/agent-md.js +84 -67
  39. package/dist/agent-md.js.map +1 -1
  40. package/dist/aid-store.d.ts +13 -0
  41. package/dist/aid-store.js +50 -15
  42. package/dist/aid-store.js.map +1 -1
  43. package/dist/auth.js +4 -0
  44. package/dist/auth.js.map +1 -1
  45. package/dist/cert-utils.d.ts +1 -0
  46. package/dist/cert-utils.js +13 -0
  47. package/dist/cert-utils.js.map +1 -1
  48. package/dist/client/delivery.d.ts +59 -1
  49. package/dist/client/delivery.js +1018 -125
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/rpc-pipeline.d.ts +4 -1
  52. package/dist/client/rpc-pipeline.js +123 -37
  53. package/dist/client/rpc-pipeline.js.map +1 -1
  54. package/dist/client/v2-e2ee.d.ts +12 -2
  55. package/dist/client/v2-e2ee.js +751 -294
  56. package/dist/client/v2-e2ee.js.map +1 -1
  57. package/dist/client.d.ts +26 -2
  58. package/dist/client.js +379 -63
  59. package/dist/client.js.map +1 -1
  60. package/dist/config.d.ts +4 -0
  61. package/dist/config.js +1 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/events.d.ts +18 -1
  64. package/dist/events.js +148 -9
  65. package/dist/events.js.map +1 -1
  66. package/dist/facades.d.ts +43 -1
  67. package/dist/facades.js +363 -7
  68. package/dist/facades.js.map +1 -1
  69. package/dist/index.d.ts +5 -3
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/keystore/aid-db.d.ts +6 -1
  73. package/dist/keystore/aid-db.js +184 -42
  74. package/dist/keystore/aid-db.js.map +1 -1
  75. package/dist/keystore/index.d.ts +14 -0
  76. package/dist/keystore/local-token-store.d.ts +9 -1
  77. package/dist/keystore/local-token-store.js +79 -14
  78. package/dist/keystore/local-token-store.js.map +1 -1
  79. package/dist/org-protocol.d.ts +45 -0
  80. package/dist/org-protocol.js +754 -11
  81. package/dist/org-protocol.js.map +1 -1
  82. package/dist/ownership-v21.d.ts +16 -3
  83. package/dist/ownership-v21.js +35 -17
  84. package/dist/ownership-v21.js.map +1 -1
  85. package/dist/ownership-workflow.d.ts +17 -0
  86. package/dist/ownership-workflow.js +511 -26
  87. package/dist/ownership-workflow.js.map +1 -1
  88. package/dist/secret-store/file-store.js +84 -0
  89. package/dist/secret-store/file-store.js.map +1 -1
  90. package/dist/seq-tracker.d.ts +1 -0
  91. package/dist/seq-tracker.js +2 -2
  92. package/dist/seq-tracker.js.map +1 -1
  93. package/dist/tools/cross-sdk-agent.d.ts +66 -3
  94. package/dist/tools/cross-sdk-agent.js +466 -36
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts +3 -0
  97. package/dist/transport.js +98 -44
  98. package/dist/transport.js.map +1 -1
  99. package/dist/types.d.ts +1 -0
  100. package/dist/v2/e2ee/encrypt-group.js +1 -0
  101. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  102. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  103. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  104. package/dist/v2/e2ee/types.d.ts +1 -0
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. 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)
@@ -107,7 +115,7 @@ export function parseOwnershipMessage(content, context) {
107
115
  if (ns.protocol !== '2.1')
108
116
  throw ownershipFailure('unsupported_protocol', 'NS ownership protocol is unavailable');
109
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);
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);
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,17 +516,50 @@ 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;
562
+ const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
318
563
  if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version)
319
564
  throw new Error('ownership original statement is corrupt');
320
565
  const owner = record.base_owner_md;
@@ -371,7 +616,7 @@ export class OwnershipWorkflow {
371
616
  if (record.status === 'completed') {
372
617
  const evidence = record.completion_evidence;
373
618
  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))
619
+ 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
620
  throw new Error('ownership target acceptance evidence is corrupt');
376
621
  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
622
  throw new Error('ownership target file evidence is missing');
@@ -381,8 +626,194 @@ export class OwnershipWorkflow {
381
626
  return { record, revision: snapshot.revision };
382
627
  }
383
628
  catch (error) {
384
- throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
629
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
630
+ throw error;
631
+ throw new OwnershipTaskDecodeError(snapshot.operation_id, error instanceof Error ? error.message : String(error));
632
+ }
633
+ }
634
+ legacyTaskView(record) {
635
+ 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);
636
+ }
637
+ legacyView(record) {
638
+ 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'))
639
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
640
+ const view = JSON.parse(JSON.stringify(record));
641
+ view.request_verified = false;
642
+ view.files = {};
643
+ view.inbox = record.inbox?.map((item) => ({ from: item.from, payload: this.legacyMessage(item.payload) }));
644
+ view.outbox = record.outbox?.map((item) => ({ to: item.to, payload: this.legacyMessage(item.payload) }));
645
+ this.decode({ operation_id: record.operation_id, revision: 1, task: view });
646
+ for (const notice of record.notices) {
647
+ if (typeof notice !== 'string' || notice.split('|').length !== 2)
648
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership notice is corrupt');
649
+ const [to, action] = notice.split('|');
650
+ 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 } });
651
+ }
652
+ const saved = record.files.owner;
653
+ if (saved) {
654
+ 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))
655
+ throw ownershipFailure('unsupported_protocol', 'legacy owner candidate cannot be recovered automatically');
656
+ const file = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
657
+ if (file.action !== 'declare' || file.statement?.relation_version !== 1)
658
+ throw ownershipFailure('unsupported_protocol', 'legacy owner candidate does not match this task');
659
+ }
660
+ return view;
661
+ }
662
+ legacyMessage(content) {
663
+ if (!content || typeof content !== 'object' || Array.isArray(content))
664
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership message is corrupt');
665
+ return { ...content };
666
+ }
667
+ async upgradeLegacy(handle) {
668
+ if (Boolean(handle.record.base_agent_md) || !handle.record.request_verified || ['completed', 'stopped', 'rejected'].includes(handle.record.status))
669
+ return;
670
+ 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'))
671
+ return this.upgradePrior(handle);
672
+ const original = JSON.parse(JSON.stringify(handle.record));
673
+ const record = this.legacyView(original);
674
+ for (const snapshot of this.list(true)) {
675
+ if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
676
+ continue;
677
+ const other = this.decode(snapshot).record;
678
+ if (!other.base_agent_md && other.request_verified && !['completed', 'stopped', 'rejected'].includes(other.status))
679
+ this.legacyTaskView(other);
680
+ if (other.status === 'pending' || other.status === 'error' && other.retryable)
681
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
682
+ }
683
+ const ns = await this.readNs(record.sub_aid);
684
+ if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
685
+ throw ownershipFailure('invalid', 'human cannot declare an owner');
686
+ if (ns.lock === 'locked' && ns.owner_aid === record.owner_aid && ns.relation_version === record.target_relation_version)
687
+ return this.upgradePrior(handle);
688
+ if (ns.lock !== 'unlocked' || ns.relation_version !== 0 || ns.owner_aid || ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement)
689
+ throw ownershipFailure('conflict', 'legacy ownership relation has changed');
690
+ const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
691
+ if (current.exists) {
692
+ const file = await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, record.owner_aid);
693
+ if (file.action !== 'declare' || file.statement?.relation_version !== 1)
694
+ throw ownershipFailure('conflict', 'legacy owner declaration has changed');
695
+ }
696
+ const candidate = original.files.owner;
697
+ if (candidate) {
698
+ await this.deps.files.verifyFile(candidate.raw, 'owner', record.sub_aid, record.owner_aid);
699
+ if (!current.exists || current.content !== candidate.raw || current.sha256 !== candidate.hash)
700
+ throw ownershipFailure('conflict', 'legacy owner candidate was replaced');
701
+ if (candidate.published_revision !== undefined && current.revision !== candidate.published_revision)
702
+ throw ownershipFailure(current.revision ? 'conflict' : 'unavailable', 'legacy published owner revision changed', !current.revision);
703
+ }
704
+ else if (this.local() === record.owner_aid && current.exists)
705
+ throw ownershipFailure('unsupported_protocol', 'legacy owner declaration lacks a local task candidate');
706
+ const owner = { exists: current.exists, raw: current.content, sha256: current.exists ? ownershipSha256(current.content) : '', ...(candidate && current.revision ? { revision: current.revision } : {}) };
707
+ record.status = 'pending';
708
+ record.step = 'approval';
709
+ record.retryable = true;
710
+ record.request_verified = true;
711
+ delete record.code;
712
+ delete record.reason;
713
+ record.inbox = [];
714
+ record.received = [];
715
+ record.outbox = [];
716
+ record.notices = [];
717
+ await this.captureBase(record, ns);
718
+ if (candidate || current.exists && this.local() === record.sub_aid)
719
+ record.files.owner = { raw: candidate?.raw ?? current.content, hash: candidate?.hash ?? current.sha256, condition: {}, original_owner: owner };
720
+ 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 };
721
+ if (this.local() === record.sub_aid)
722
+ this.enqueue(record, record.owner_aid, 'bind.request');
723
+ else if (record.local_decision === 'accept')
724
+ this.enqueue(record, record.sub_aid, 'bind.accept');
725
+ const after = await this.readNs(record.sub_aid), afterOwner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
726
+ 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)
727
+ throw ownershipFailure('conflict', 'legacy recovery observations changed');
728
+ const recovered = { record, revision: handle.revision };
729
+ this.decode({ operation_id: record.operation_id, revision: handle.revision + 1, task: record });
730
+ try {
731
+ await this.save(recovered);
385
732
  }
733
+ catch (error) {
734
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable' && error.message.includes('active ownership task already exists'))
735
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
736
+ throw error;
737
+ }
738
+ handle.record = record;
739
+ handle.revision = recovered.revision;
740
+ }
741
+ priorView(original) {
742
+ 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'))
743
+ throw ownershipFailure('unsupported_protocol', 'legacy task context cannot be recovered');
744
+ const record = structuredClone(original);
745
+ record.request_verified = false;
746
+ for (const [key, saved] of Object.entries(record.files ?? {})) {
747
+ const owner = key === 'owner' ? targetOwner(record) : record.owner_aid;
748
+ if (this.local() !== (key === 'agent' ? record.sub_aid : owner))
749
+ throw ownershipFailure('unsupported_protocol', 'legacy file local role is invalid');
750
+ if (key !== 'agent' && key !== 'cleanup') {
751
+ const condition = saved.condition;
752
+ if (!saved.original_owner && (!condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision))
753
+ throw ownershipFailure('unsupported_protocol', 'legacy owner condition is invalid');
754
+ saved.condition = {};
755
+ saved.original_owner ??= { exists: false, raw: '', sha256: '' };
756
+ }
757
+ }
758
+ try {
759
+ this.decode({ operation_id: original.operation_id, revision: 1, task: record });
760
+ }
761
+ catch (error) {
762
+ throw ownershipFailure('unsupported_protocol', error instanceof Error ? error.message : String(error));
763
+ }
764
+ return record;
765
+ }
766
+ async upgradePrior(handle) {
767
+ const original = structuredClone(handle.record), record = this.priorView(original);
768
+ for (const snapshot of this.list(true)) {
769
+ if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
770
+ continue;
771
+ const other = this.decode(snapshot).record;
772
+ if (other.status === 'pending' || other.status === 'error' && other.retryable)
773
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
774
+ }
775
+ const ns = await this.readNs(record.sub_aid);
776
+ if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
777
+ throw ownershipFailure('invalid', 'human cannot declare an owner');
778
+ 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);
779
+ const released = record.operation === 'release' && ns.lock === 'unlocked' && !ns.owner_aid && ns.relation_version === record.base_relation_version;
780
+ const committed = record.operation !== 'release' && ns.lock === 'locked' && ns.owner_aid === targetOwner(record) && ns.relation_version === record.target_relation_version;
781
+ if (!base && !released && !committed)
782
+ throw ownershipFailure('conflict', 'legacy relationship no longer matches this task');
783
+ if (!committed)
784
+ await this.captureBase(record, ns);
785
+ for (const [key, saved] of Object.entries(record.files)) {
786
+ const owner = key === 'owner' || key === 'agent' ? targetOwner(record) : record.owner_aid;
787
+ await this.deps.files.verifyFile(saved.raw, key === 'agent' ? 'agent' : 'owner', record.sub_aid, owner);
788
+ if (key === 'agent')
789
+ continue;
790
+ const current = await this.deps.files.readOwner(owner, record.sub_aid);
791
+ const source = original.files[key];
792
+ const revision = source.condition.expected_revision;
793
+ if (revision && (current.content === saved.raw && source.published_revision !== undefined || !committed && current.content !== saved.raw)) {
794
+ if (!current.revision)
795
+ throw ownershipFailure('unavailable', 'legacy owner original revision is unavailable', true);
796
+ if (current.revision !== (current.content === saved.raw ? source.published_revision : revision))
797
+ throw ownershipFailure('conflict', 'legacy owner original revision changed');
798
+ }
799
+ const currentStatement = current.exists ? parseOwnershipFile(current.content, 'owner', { expectedSubAid: record.sub_aid }).statement : undefined;
800
+ const expected = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid }).statement;
801
+ const same = expected && currentStatement && Object.keys(expected).every((field) => expected[field] === currentStatement[field]);
802
+ if (current.content !== saved.raw && (source.published_revision !== undefined || current.exists && (key === 'owner' || !same)) && !committed)
803
+ throw ownershipFailure('conflict', 'legacy signed owner file was replaced');
804
+ saved.original_owner = { exists: current.exists, raw: current.content, sha256: current.exists ? current.sha256 : '', ...(revision && current.revision ? { revision: current.revision } : {}) };
805
+ }
806
+ record.request_verified = true;
807
+ 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 } };
808
+ const recovered = { record, revision: handle.revision };
809
+ if (committed) {
810
+ if (!await this.confirm(recovered))
811
+ throw ownershipFailure('conflict', 'legacy committed target lacks matching local signed evidence');
812
+ }
813
+ else
814
+ await this.save(recovered);
815
+ handle.record = record;
816
+ handle.revision = recovered.revision;
386
817
  }
387
818
  async save(handle) {
388
819
  try {
@@ -454,17 +885,36 @@ export class OwnershipWorkflow {
454
885
  async start(operation, subAid, ownerAid, newOwnerAid, operationId) {
455
886
  this.requireApplicable(subAid);
456
887
  return this.runSerialized(subAid, async () => {
888
+ if (operation === 'bind' && !operationId)
889
+ await this.resetUnusableTasks(subAid, ownerAid);
457
890
  const existing = operationId ? this.load(operationId) : null;
458
891
  if (existing) {
892
+ if (this.isReset(existing.record))
893
+ throw ownershipFailure('conflict', 'ownership task was reset');
894
+ await this.upgradeLegacy(existing);
459
895
  const record = existing.record;
896
+ if (!record.base_agent_md && record.request_verified)
897
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
460
898
  if (record.operation !== operation || record.sub_aid !== subAid || record.owner_aid !== ownerAid || record.new_owner_aid !== newOwnerAid || record.initiator_aid !== this.local())
461
899
  throw ownershipFailure('conflict', 'saved registration task context changed');
462
900
  await this.drive(existing);
463
901
  return ownershipTaskResult(record);
464
902
  }
465
903
  let ns;
466
- for (const snapshot of this.list()) {
467
- const active = this.decode(snapshot), record = active.record;
904
+ for (const snapshot of this.list(true)) {
905
+ if (snapshot.task.sub_aid !== subAid)
906
+ continue;
907
+ const active = this.decode(snapshot);
908
+ if (this.isReset(active.record))
909
+ continue;
910
+ if (!active.record.base_agent_md && active.record.request_verified && ['completed', 'stopped', 'rejected'].includes(active.record.status))
911
+ continue;
912
+ 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()))
913
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
914
+ await this.upgradeLegacy(active);
915
+ const record = active.record;
916
+ if (!record.base_agent_md && record.request_verified)
917
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
468
918
  if (record.sub_aid !== subAid || record.status !== 'pending' && !(record.status === 'error' && record.retryable))
469
919
  continue;
470
920
  const same = record.operation === operation && record.owner_aid === ownerAid && record.new_owner_aid === newOwnerAid && record.initiator_aid === this.local();
@@ -488,7 +938,7 @@ export class OwnershipWorkflow {
488
938
  base = false;
489
939
  }
490
940
  }
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);
941
+ 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
942
  if (!base && !target) {
493
943
  await this.recordError(active, ownershipFailure('conflict', 'the operation was superseded by a different relation result'));
494
944
  const result = ownershipTaskResult(record);
@@ -508,11 +958,11 @@ export class OwnershipWorkflow {
508
958
  ns = await this.readNs(subAid);
509
959
  if (operation === 'bind' && ns.lock !== 'unlocked')
510
960
  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')
961
+ if (!ns.exists && operation !== 'bind')
962
+ throw ownershipFailure('conflict', 'original agent.md is missing');
963
+ if (operation === 'bind' && (ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type : subAid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
514
964
  throw ownershipFailure('invalid', 'human cannot declare an owner');
515
- if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
965
+ if (operation === 'bind' && ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
516
966
  throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
517
967
  if (operation !== 'bind') {
518
968
  const consensus = await this.deps.files.checkConsensus(subAid);
@@ -536,9 +986,14 @@ export class OwnershipWorkflow {
536
986
  const initial = this.load(operationId);
537
987
  if (!initial)
538
988
  throw ownershipFailure('operation_not_found', 'no local ownership request matches operation_id');
989
+ if (this.isReset(initial.record))
990
+ throw ownershipFailure('conflict', 'ownership task was reset');
539
991
  return this.runSerialized(initial.record.sub_aid, async () => {
540
992
  const handle = this.load(operationId);
993
+ await this.upgradeLegacy(handle);
541
994
  const record = handle.record;
995
+ if (!record.base_agent_md && record.request_verified)
996
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
542
997
  if (record.operation === 'release' || record.initiator_aid === this.local() || !record.request_verified)
543
998
  throw ownershipFailure('forbidden', 'this task is not a verified local approval request');
544
999
  if (record.local_decision !== 'pending') {
@@ -598,7 +1053,7 @@ export class OwnershipWorkflow {
598
1053
  throw new Error('ownership outgoing action lacks participant approvals');
599
1054
  }
600
1055
  catch (error) {
601
- throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
1056
+ throw new OwnershipTaskDecodeError(record.operation_id, error instanceof Error ? error.message : String(error));
602
1057
  }
603
1058
  }
604
1059
  async flush(handle) {
@@ -613,7 +1068,7 @@ export class OwnershipWorkflow {
613
1068
  }
614
1069
  const ns = await this.readNs(record.sub_aid);
615
1070
  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);
1071
+ 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
1072
  const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
618
1073
  if (requiresTarget ? !target : !base) {
619
1074
  record.outbox.shift();
@@ -629,6 +1084,7 @@ export class OwnershipWorkflow {
629
1084
  if (expiresAt <= Math.trunc(Date.now() / 1000))
630
1085
  throw ownershipFailure('conflict', 'transfer authorization expired');
631
1086
  }
1087
+ this.assertActive(handle);
632
1088
  await this.deps.send(message.to, message.payload);
633
1089
  handle.record.outbox.shift();
634
1090
  await this.save(handle);
@@ -645,7 +1101,7 @@ export class OwnershipWorkflow {
645
1101
  return { ...ns, owner_aid: currentOwner };
646
1102
  }
647
1103
  async captureBase(record, ns) {
648
- const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
1104
+ const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
649
1105
  record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement };
650
1106
  if (!statement) {
651
1107
  record.base_owner_md = { exists: false, raw: '', sha256: '' };
@@ -693,9 +1149,9 @@ export class OwnershipWorkflow {
693
1149
  }
694
1150
  assertAgentBaseMaterial(record, ns) {
695
1151
  const base = record.base_agent_md;
696
- if (!base)
1152
+ if (!base || typeof base.raw !== 'string')
697
1153
  throw ownershipFailure('persistence_unavailable', 'original agent.md evidence is missing');
698
- const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
1154
+ const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
699
1155
  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
1156
  if (ns.exists !== base.exists || !same)
701
1157
  throw ownershipFailure('conflict', 'the original ownership statement was replaced');
@@ -723,6 +1179,17 @@ export class OwnershipWorkflow {
723
1179
  continue;
724
1180
  }
725
1181
  const action = item.payload.action;
1182
+ if (action === 'owner.ready' && record.legacy_recovery && record.status === 'pending') {
1183
+ const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
1184
+ if (!owner.exists)
1185
+ throw ownershipFailure(record.files.owner ? 'conflict' : 'unavailable', 'legacy owner confirmation declaration is missing', !record.files.owner);
1186
+ const file = await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
1187
+ 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))
1188
+ throw ownershipFailure('conflict', 'legacy owner confirmation declaration changed');
1189
+ if (!record.files.owner)
1190
+ record.files.owner = { raw: owner.content, hash: owner.sha256, condition: {}, original_owner: { exists: true, raw: owner.content, sha256: owner.sha256 } };
1191
+ record.legacy_recovery.owner_confirmed = true;
1192
+ }
726
1193
  if (['bind.request', 'transfer.request'].includes(action)) {
727
1194
  if (!record.request_verified)
728
1195
  await this.captureBase(record, ns);
@@ -844,6 +1311,7 @@ export class OwnershipWorkflow {
844
1311
  const task = { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid,
845
1312
  new_owner_aid: record.new_owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version,
846
1313
  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,
1314
+ assertActive: () => this.assertActive(handle),
847
1315
  persist: async () => {
848
1316
  record.authorization_sha256 = task.authorization_sha256;
849
1317
  record.expires_at = task.expires_at;
@@ -853,8 +1321,18 @@ export class OwnershipWorkflow {
853
1321
  } };
854
1322
  return task;
855
1323
  }
1324
+ assertActive(handle) {
1325
+ const current = this.load(handle.record.operation_id);
1326
+ if (!current || this.isReset(current.record) || current.revision !== handle.revision)
1327
+ throw ownershipFailure('conflict', 'ownership task changed before external action');
1328
+ }
856
1329
  async drive(handle) {
1330
+ if (this.isReset(handle.record))
1331
+ return;
1332
+ await this.upgradeLegacy(handle);
857
1333
  const record = handle.record;
1334
+ if (!record.base_agent_md && record.request_verified)
1335
+ return;
858
1336
  if (record.status === 'error' && record.retryable) {
859
1337
  record.status = 'pending';
860
1338
  await this.save(handle);
@@ -907,6 +1385,8 @@ export class OwnershipWorkflow {
907
1385
  const approved = participants(record).every((name) => record.approvals[name] === 'accept');
908
1386
  if (record.operation === 'bind' && !approved || record.operation === 'transfer' && this.local() === record.owner_aid && !approved)
909
1387
  return;
1388
+ if (record.legacy_recovery && this.local() === record.sub_aid && !record.legacy_recovery.owner_confirmed)
1389
+ return;
910
1390
  delete record.code;
911
1391
  delete record.reason;
912
1392
  record.retryable = true;
@@ -937,6 +1417,11 @@ export class OwnershipWorkflow {
937
1417
  await this.flush(handle);
938
1418
  }
939
1419
  if (this.local() === record.sub_aid) {
1420
+ if (record.legacy_recovery && record.files.owner) {
1421
+ const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid), pinned = record.files.owner;
1422
+ if (!current.exists || current.content !== pinned.raw || current.sha256 !== pinned.hash)
1423
+ throw ownershipFailure('conflict', 'legacy owner declaration changed before subject publication');
1424
+ }
940
1425
  record.step = 'agent';
941
1426
  await this.save(handle);
942
1427
  await this.deps.files.declareOwneraid(this.fileTask(handle));
@@ -987,7 +1472,7 @@ export class OwnershipWorkflow {
987
1472
  record.cleanup_status = 'cleaned';
988
1473
  cleaned = true;
989
1474
  }
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));
1475
+ 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
1476
  if (!reached)
992
1477
  return false;
993
1478
  if (record.operation === 'release' && !cleaned) {
@@ -1106,7 +1591,7 @@ export class OwnershipWorkflow {
1106
1591
  }
1107
1592
  await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: record.sub_aid, owner_aid: targetOwner(record),
1108
1593
  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) });
1594
+ files: record.release_cleanup.files, expected_agent_statement: original, assertActive: () => this.assertActive(handle), persist: () => this.save(handle) });
1110
1595
  record.release_cleanup.cleaned = true;
1111
1596
  await this.save(handle);
1112
1597
  this.emit(record, 'ownership.progress');
@@ -1169,8 +1654,8 @@ export class OwnershipWorkflow {
1169
1654
  const payload = { ...ownershipTaskResult(record), local_roles: [...record.local_roles], ...extra };
1170
1655
  if (event === 'ownership.request')
1171
1656
  payload.requester_aid = record.initiator_aid;
1172
- if (record.verified_relation_version !== undefined)
1173
- payload.relation_version = record.verified_relation_version;
1657
+ if (record.completion_evidence !== undefined)
1658
+ payload.relation_version = record.completion_evidence.relation_version;
1174
1659
  queueMicrotask(() => { try {
1175
1660
  const pending = this.deps.emit(event, payload);
1176
1661
  if (pending)