dsh-kingdom 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +267 -246
  2. package/docs/CURRENT-USER-GUIDE.md +106 -0
  3. package/docs/DISPATCH-IDENTITY-PROBE.md +79 -0
  4. package/docs/V3.2-RELEASE-NOTES.md +108 -103
  5. package/docs/V3.2.1-RELEASE-NOTES.md +39 -0
  6. package/docs/V3.3-RELEASE-NOTES.md +46 -0
  7. package/lib/adapter/dsh-backend.js +189 -11
  8. package/lib/adapter/dsh-backend.js.map +1 -1
  9. package/lib/adapter/dsh-session-events.js +51 -7
  10. package/lib/adapter/dsh-session-events.js.map +1 -1
  11. package/lib/core/db.js +557 -546
  12. package/lib/core/db.js.map +1 -1
  13. package/lib/core/governed.js +20 -6
  14. package/lib/core/governed.js.map +1 -1
  15. package/lib/core/owner-binding-intent.js +168 -11
  16. package/lib/core/owner-binding-intent.js.map +1 -1
  17. package/lib/core/task-service.js +4 -1
  18. package/lib/core/task-service.js.map +1 -1
  19. package/lib/core/usage.js +10 -6
  20. package/lib/core/usage.js.map +1 -1
  21. package/lib/core/workspace-admission.js +31 -7
  22. package/lib/core/workspace-admission.js.map +1 -1
  23. package/lib/dispatch/service.js +141 -7
  24. package/lib/dispatch/service.js.map +1 -1
  25. package/lib/gui/console-app.js +34 -30
  26. package/lib/gui/console-app.js.map +1 -1
  27. package/lib/gui/local-control.js +59 -3
  28. package/lib/gui/local-control.js.map +1 -1
  29. package/lib/gui/owner-ui.js +71 -8
  30. package/lib/gui/owner-ui.js.map +1 -1
  31. package/lib/gui/server.js +1 -1
  32. package/lib/gui/server.js.map +1 -1
  33. package/lib/gui/workbench-ui.js +179 -10
  34. package/lib/gui/workbench-ui.js.map +1 -1
  35. package/lib/index.js +52 -13
  36. package/lib/index.js.map +1 -1
  37. package/lib/types/adapter/contract.d.ts +20 -0
  38. package/lib/types/adapter/dsh-backend.d.ts +5 -1
  39. package/lib/types/adapter/dsh-session-events.d.ts +10 -1
  40. package/lib/types/core/db.d.ts +2 -0
  41. package/lib/types/core/governed.d.ts +5 -0
  42. package/lib/types/core/owner-binding-intent.d.ts +26 -0
  43. package/lib/types/core/workspace-admission.d.ts +13 -1
  44. package/lib/types/dispatch/service.d.ts +18 -14
  45. package/lib/types/gui/console-app.d.ts +1 -1
  46. package/lib/types/gui/control-contract.d.ts +5 -3
  47. package/lib/types/gui/local-control.d.ts +14 -2
  48. package/lib/types/gui/owner-ui.d.ts +31 -0
  49. package/lib/worker/governed-executor.js +18 -0
  50. package/lib/worker/governed-executor.js.map +1 -1
  51. package/package.json +10 -4
package/lib/core/db.js CHANGED
@@ -44,118 +44,118 @@ import { asExecutionState, isTerminalExecutionState, transitionExecution, } from
44
44
  * - v4 DDL 不进 SCHEMA_SQL bootstrap(避免迁移事务语义失效;见 ensureSchemaV4 头注)。
45
45
  */
46
46
  export const SCHEMA_VERSION = 4;
47
- export const SCHEMA_SQL = `
48
- CREATE TABLE IF NOT EXISTS kingdoms (
49
- kingdom_id TEXT PRIMARY KEY,
50
- name TEXT NOT NULL,
51
- created_at TEXT NOT NULL,
52
- owner_id TEXT NOT NULL,
53
- owner_name TEXT NOT NULL,
54
- schema_version INTEGER NOT NULL DEFAULT 1
55
- );
56
-
57
- CREATE TABLE IF NOT EXISTS territories (
58
- territory_id TEXT PRIMARY KEY,
59
- kingdom_id TEXT NOT NULL,
60
- name TEXT NOT NULL,
61
- workspace_path TEXT,
62
- summary TEXT,
63
- supervisor_binding_id TEXT,
64
- status TEXT NOT NULL DEFAULT 'ACTIVE',
65
- created_at TEXT NOT NULL
66
- );
67
-
68
- CREATE TABLE IF NOT EXISTS role_bindings (
69
- binding_id TEXT PRIMARY KEY,
70
- kingdom_id TEXT NOT NULL,
71
- role_type TEXT NOT NULL CHECK(role_type IN ('OWNER','CHANCELLOR','SUPERVISOR','WORKER')),
72
- role_name TEXT NOT NULL,
73
- runtime_type TEXT NOT NULL DEFAULT 'dsh',
74
- session_id TEXT,
75
- model_name TEXT,
76
- agent_name TEXT,
77
- session_meta TEXT,
78
- principal_id TEXT,
79
- created_at TEXT NOT NULL,
80
- updated_at TEXT NOT NULL
81
- );
82
-
83
- CREATE TABLE IF NOT EXISTS tasks (
84
- task_id TEXT PRIMARY KEY,
85
- territory_id TEXT NOT NULL,
86
- parent_task_id TEXT,
87
- title TEXT NOT NULL,
88
- description TEXT,
89
- assigned_binding_id TEXT,
90
- status TEXT NOT NULL DEFAULT 'CREATED',
91
- acceptance_criteria TEXT,
92
- result_summary TEXT,
93
- created_at TEXT NOT NULL,
94
- updated_at TEXT NOT NULL
95
- );
96
-
97
- CREATE TABLE IF NOT EXISTS events (
98
- event_id TEXT PRIMARY KEY,
99
- kingdom_id TEXT NOT NULL,
100
- event_type TEXT NOT NULL,
101
- actor_role TEXT,
102
- actor_id TEXT,
103
- target_type TEXT,
104
- target_id TEXT,
105
- payload_json TEXT NOT NULL DEFAULT '{}',
106
- created_at TEXT NOT NULL
107
- );
108
-
109
- CREATE TABLE IF NOT EXISTS worker_results (
110
- result_id TEXT PRIMARY KEY,
111
- task_id TEXT NOT NULL,
112
- attempt_no INTEGER NOT NULL,
113
- worker_binding_id TEXT,
114
- session_id TEXT,
115
- outcome TEXT NOT NULL,
116
- result_json TEXT NOT NULL DEFAULT '{}',
117
- created_at TEXT NOT NULL,
118
- UNIQUE(task_id, attempt_no)
119
- );
120
-
121
- CREATE UNIQUE INDEX IF NOT EXISTS territories_kingdom_name_uk
122
- ON territories(kingdom_id, name);
123
-
124
- CREATE TABLE IF NOT EXISTS executions (
125
- execution_id TEXT PRIMARY KEY,
126
- task_id TEXT NOT NULL,
127
- attempt_no INTEGER NOT NULL,
128
- worker_binding_id TEXT,
129
- session_id TEXT,
130
- state TEXT NOT NULL,
131
- detail TEXT,
132
- started_at TEXT NOT NULL,
133
- heartbeat_at TEXT,
134
- ended_at TEXT,
135
- pause_requested_at TEXT,
136
- UNIQUE(task_id, attempt_no)
137
- );
138
-
139
- CREATE INDEX IF NOT EXISTS executions_task_idx ON executions(task_id);
140
-
141
- CREATE TABLE IF NOT EXISTS task_assignments (
142
- assignment_id TEXT PRIMARY KEY,
143
- task_id TEXT NOT NULL,
144
- territory_id TEXT NOT NULL,
145
- worker_binding_id TEXT NOT NULL,
146
- assigned_by TEXT NOT NULL,
147
- assigned_at TEXT NOT NULL,
148
- ended_at TEXT,
149
- end_reason TEXT,
150
- previous_assignment_id TEXT,
151
- handoff_reason TEXT,
152
- created_at TEXT NOT NULL
153
- );
154
-
155
- CREATE UNIQUE INDEX IF NOT EXISTS one_active_assignment_per_task
156
- ON task_assignments(task_id) WHERE ended_at IS NULL;
157
-
158
- CREATE INDEX IF NOT EXISTS task_assignments_task_idx ON task_assignments(task_id);
47
+ export const SCHEMA_SQL = `
48
+ CREATE TABLE IF NOT EXISTS kingdoms (
49
+ kingdom_id TEXT PRIMARY KEY,
50
+ name TEXT NOT NULL,
51
+ created_at TEXT NOT NULL,
52
+ owner_id TEXT NOT NULL,
53
+ owner_name TEXT NOT NULL,
54
+ schema_version INTEGER NOT NULL DEFAULT 1
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS territories (
58
+ territory_id TEXT PRIMARY KEY,
59
+ kingdom_id TEXT NOT NULL,
60
+ name TEXT NOT NULL,
61
+ workspace_path TEXT,
62
+ summary TEXT,
63
+ supervisor_binding_id TEXT,
64
+ status TEXT NOT NULL DEFAULT 'ACTIVE',
65
+ created_at TEXT NOT NULL
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS role_bindings (
69
+ binding_id TEXT PRIMARY KEY,
70
+ kingdom_id TEXT NOT NULL,
71
+ role_type TEXT NOT NULL CHECK(role_type IN ('OWNER','CHANCELLOR','SUPERVISOR','WORKER')),
72
+ role_name TEXT NOT NULL,
73
+ runtime_type TEXT NOT NULL DEFAULT 'dsh',
74
+ session_id TEXT,
75
+ model_name TEXT,
76
+ agent_name TEXT,
77
+ session_meta TEXT,
78
+ principal_id TEXT,
79
+ created_at TEXT NOT NULL,
80
+ updated_at TEXT NOT NULL
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS tasks (
84
+ task_id TEXT PRIMARY KEY,
85
+ territory_id TEXT NOT NULL,
86
+ parent_task_id TEXT,
87
+ title TEXT NOT NULL,
88
+ description TEXT,
89
+ assigned_binding_id TEXT,
90
+ status TEXT NOT NULL DEFAULT 'CREATED',
91
+ acceptance_criteria TEXT,
92
+ result_summary TEXT,
93
+ created_at TEXT NOT NULL,
94
+ updated_at TEXT NOT NULL
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS events (
98
+ event_id TEXT PRIMARY KEY,
99
+ kingdom_id TEXT NOT NULL,
100
+ event_type TEXT NOT NULL,
101
+ actor_role TEXT,
102
+ actor_id TEXT,
103
+ target_type TEXT,
104
+ target_id TEXT,
105
+ payload_json TEXT NOT NULL DEFAULT '{}',
106
+ created_at TEXT NOT NULL
107
+ );
108
+
109
+ CREATE TABLE IF NOT EXISTS worker_results (
110
+ result_id TEXT PRIMARY KEY,
111
+ task_id TEXT NOT NULL,
112
+ attempt_no INTEGER NOT NULL,
113
+ worker_binding_id TEXT,
114
+ session_id TEXT,
115
+ outcome TEXT NOT NULL,
116
+ result_json TEXT NOT NULL DEFAULT '{}',
117
+ created_at TEXT NOT NULL,
118
+ UNIQUE(task_id, attempt_no)
119
+ );
120
+
121
+ CREATE UNIQUE INDEX IF NOT EXISTS territories_kingdom_name_uk
122
+ ON territories(kingdom_id, name);
123
+
124
+ CREATE TABLE IF NOT EXISTS executions (
125
+ execution_id TEXT PRIMARY KEY,
126
+ task_id TEXT NOT NULL,
127
+ attempt_no INTEGER NOT NULL,
128
+ worker_binding_id TEXT,
129
+ session_id TEXT,
130
+ state TEXT NOT NULL,
131
+ detail TEXT,
132
+ started_at TEXT NOT NULL,
133
+ heartbeat_at TEXT,
134
+ ended_at TEXT,
135
+ pause_requested_at TEXT,
136
+ UNIQUE(task_id, attempt_no)
137
+ );
138
+
139
+ CREATE INDEX IF NOT EXISTS executions_task_idx ON executions(task_id);
140
+
141
+ CREATE TABLE IF NOT EXISTS task_assignments (
142
+ assignment_id TEXT PRIMARY KEY,
143
+ task_id TEXT NOT NULL,
144
+ territory_id TEXT NOT NULL,
145
+ worker_binding_id TEXT NOT NULL,
146
+ assigned_by TEXT NOT NULL,
147
+ assigned_at TEXT NOT NULL,
148
+ ended_at TEXT,
149
+ end_reason TEXT,
150
+ previous_assignment_id TEXT,
151
+ handoff_reason TEXT,
152
+ created_at TEXT NOT NULL
153
+ );
154
+
155
+ CREATE UNIQUE INDEX IF NOT EXISTS one_active_assignment_per_task
156
+ ON task_assignments(task_id) WHERE ended_at IS NULL;
157
+
158
+ CREATE INDEX IF NOT EXISTS task_assignments_task_idx ON task_assignments(task_id);
159
159
  `;
160
160
  export class KingdomStore {
161
161
  db;
@@ -340,13 +340,13 @@ export class KingdomStore {
340
340
  .prepare(`SELECT COUNT(*) AS n FROM role_bindings WHERE role_type = 'SUPERVISOR' AND status = 'ACTIVE'`)
341
341
  .get();
342
342
  if (supervisorCount.n === 1) {
343
- this.db.exec(`
344
- UPDATE territories
345
- SET supervisor_binding_id = (
346
- SELECT binding_id FROM role_bindings
347
- WHERE role_type = 'SUPERVISOR' AND status = 'ACTIVE' LIMIT 1
348
- )
349
- WHERE supervisor_binding_id IS NULL AND status = 'ACTIVE'
343
+ this.db.exec(`
344
+ UPDATE territories
345
+ SET supervisor_binding_id = (
346
+ SELECT binding_id FROM role_bindings
347
+ WHERE role_type = 'SUPERVISOR' AND status = 'ACTIVE' LIMIT 1
348
+ )
349
+ WHERE supervisor_binding_id IS NULL AND status = 'ACTIVE'
350
350
  `);
351
351
  }
352
352
  // verify:全部预期列/表/索引存在,缺失即失败(fail-loud)
@@ -391,351 +391,351 @@ export class KingdomStore {
391
391
  // 既有 v3 库开库瞬间就被建出 v4 表,ensureSchemaV4 的"单事务迁移 + 失败 ROLLBACK"
392
392
  // 语义失效。因此 v4 全部 DDL 只在 ensureSchemaV4 的 BEGIN IMMEDIATE 事务内落地。
393
393
  /** v4 Ledger 表(affinity/lease/decision)+ 各自索引与 trigger(不含 executions 重建与 dispatch)。 */
394
- static V4_LEDGER_SQL = `
395
- CREATE TABLE IF NOT EXISTS session_territory_affinities (
396
- affinity_id TEXT PRIMARY KEY,
397
- kingdom_id TEXT NOT NULL,
398
- worker_binding_id TEXT NOT NULL,
399
- runtime_type TEXT NOT NULL,
400
- runtime_instance_ref TEXT NOT NULL,
401
- session_ref TEXT NOT NULL,
402
- territory_id TEXT NOT NULL,
403
- established_at TEXT NOT NULL,
404
- retired_at TEXT,
405
- is_current INTEGER NOT NULL DEFAULT 1,
406
- created_at TEXT NOT NULL,
407
- UNIQUE (runtime_type, runtime_instance_ref, session_ref),
408
- CHECK (
409
- (is_current = 1 AND retired_at IS NULL)
410
- OR
411
- (is_current = 0 AND retired_at IS NOT NULL)
412
- )
413
- );
414
- CREATE UNIQUE INDEX IF NOT EXISTS affinity_one_current_per_worker
415
- ON session_territory_affinities(kingdom_id, worker_binding_id)
416
- WHERE is_current = 1;
417
- CREATE TRIGGER IF NOT EXISTS affinity_identity_immutable
418
- BEFORE UPDATE OF kingdom_id, worker_binding_id, runtime_type,
419
- runtime_instance_ref, session_ref, territory_id,
420
- established_at, created_at
421
- ON session_territory_affinities
422
- BEGIN SELECT RAISE(ABORT, 'AFFINITY_IDENTITY_IMMUTABLE'); END;
423
- CREATE TRIGGER IF NOT EXISTS affinity_retire
424
- BEFORE UPDATE OF is_current, retired_at
425
- ON session_territory_affinities
426
- BEGIN
427
- SELECT RAISE(ABORT, 'AFFINITY_RETIRE_ONLY_JOINT')
428
- WHERE NOT (
429
- OLD.is_current = 1 AND OLD.retired_at IS NULL
430
- AND NEW.is_current = 0 AND NEW.retired_at IS NOT NULL
431
- );
432
- SELECT RAISE(ABORT, 'AFFINITY_ALREADY_RETIRED')
433
- WHERE OLD.retired_at IS NOT NULL;
434
- END;
435
- CREATE TRIGGER IF NOT EXISTS affinity_no_delete
436
- BEFORE DELETE ON session_territory_affinities
437
- BEGIN SELECT RAISE(ABORT, 'AFFINITY_NO_DELETE'); END;
438
-
439
- CREATE TABLE IF NOT EXISTS execution_leases (
440
- lease_id TEXT PRIMARY KEY,
441
- kingdom_id TEXT NOT NULL,
442
- worker_binding_id TEXT NOT NULL,
443
- runtime_type TEXT NOT NULL,
444
- runtime_instance_ref TEXT NOT NULL,
445
- session_ref TEXT NOT NULL,
446
- territory_id TEXT NOT NULL,
447
- task_id TEXT NOT NULL,
448
- attempt_no INTEGER NOT NULL,
449
- state TEXT NOT NULL CHECK(state IN
450
- ('ACQUIRED','PREPARING','MATERIALIZING','DISPATCH_READY',
451
- 'EXECUTING','SETTLING','RELEASING','RECOVERING','RELEASED')),
452
- capability_decision_id TEXT,
453
- enforcement_plan_snapshot TEXT,
454
- release_evidence_json TEXT,
455
- release_reason TEXT,
456
- acquired_at TEXT NOT NULL,
457
- released_at TEXT,
458
- updated_at TEXT NOT NULL,
459
- UNIQUE (task_id, attempt_no),
460
- UNIQUE (runtime_type, runtime_instance_ref, session_ref, task_id, attempt_no)
461
- );
462
- CREATE UNIQUE INDEX IF NOT EXISTS lease_one_active_per_session
463
- ON execution_leases(runtime_type, runtime_instance_ref, session_ref)
464
- WHERE state <> 'RELEASED';
465
- CREATE TRIGGER IF NOT EXISTS lease_requires_matching_affinity
466
- BEFORE INSERT ON execution_leases
467
- BEGIN
468
- SELECT RAISE(ABORT, 'LEASE_REQUIRES_MATCHING_CURRENT_AFFINITY')
469
- WHERE NOT EXISTS (
470
- SELECT 1
471
- FROM session_territory_affinities a
472
- JOIN tasks t ON t.task_id = NEW.task_id
473
- WHERE a.kingdom_id = NEW.kingdom_id
474
- AND a.worker_binding_id = NEW.worker_binding_id
475
- AND a.runtime_type = NEW.runtime_type
476
- AND a.runtime_instance_ref = NEW.runtime_instance_ref
477
- AND a.session_ref = NEW.session_ref
478
- AND a.is_current = 1 AND a.retired_at IS NULL
479
- AND a.territory_id = NEW.territory_id
480
- AND t.territory_id = NEW.territory_id
481
- );
482
- END;
483
- CREATE TRIGGER IF NOT EXISTS lease_identity_immutable
484
- BEFORE UPDATE OF kingdom_id, worker_binding_id, runtime_type, runtime_instance_ref,
485
- session_ref, territory_id, task_id, attempt_no, acquired_at
486
- ON execution_leases
487
- BEGIN SELECT RAISE(ABORT, 'LEASE_IDENTITY_IMMUTABLE'); END;
488
- CREATE TRIGGER IF NOT EXISTS lease_plan_once
489
- BEFORE UPDATE OF enforcement_plan_snapshot
490
- ON execution_leases
491
- WHEN OLD.enforcement_plan_snapshot IS NOT NULL
492
- BEGIN SELECT RAISE(ABORT, 'LEASE_PLAN_ALREADY_SET'); END;
493
- CREATE TRIGGER IF NOT EXISTS lease_decision_once
494
- BEFORE UPDATE OF capability_decision_id
495
- ON execution_leases
496
- WHEN OLD.capability_decision_id IS NOT NULL
497
- BEGIN SELECT RAISE(ABORT, 'LEASE_DECISION_ALREADY_BOUND'); END;
498
- CREATE TRIGGER IF NOT EXISTS lease_release_evidence_once
499
- BEFORE UPDATE OF release_evidence_json, release_reason
500
- ON execution_leases
501
- WHEN OLD.release_evidence_json IS NOT NULL
502
- BEGIN SELECT RAISE(ABORT, 'LEASE_RELEASE_EVIDENCE_ALREADY_SET'); END;
503
- CREATE TRIGGER IF NOT EXISTS lease_state_guard
504
- BEFORE UPDATE OF state ON execution_leases
505
- WHEN NEW.state <> OLD.state
506
- BEGIN
507
- SELECT RAISE(ABORT, 'ILLEGAL_LEASE_TRANSITION') WHERE NOT (
508
- (OLD.state='ACQUIRED' AND NEW.state IN ('PREPARING','RECOVERING'))
509
- OR (OLD.state='PREPARING' AND NEW.state IN ('MATERIALIZING','RECOVERING'))
510
- OR (OLD.state='MATERIALIZING' AND NEW.state IN ('DISPATCH_READY','RECOVERING','RELEASED'))
511
- OR (OLD.state='DISPATCH_READY' AND NEW.state IN ('EXECUTING','RECOVERING'))
512
- OR (OLD.state='EXECUTING' AND NEW.state IN ('SETTLING','RECOVERING'))
513
- OR (OLD.state='SETTLING' AND NEW.state IN ('RELEASING','RECOVERING'))
514
- OR (OLD.state='RELEASING' AND NEW.state IN ('RELEASED','RECOVERING'))
515
- OR (OLD.state='RECOVERING' AND NEW.state IN ('RELEASED','RECOVERING'))
516
- );
517
- SELECT RAISE(ABORT, 'LEASE_MATERIALIZING_REQUIRES_PLAN')
518
- WHERE NEW.state='MATERIALIZING' AND (NEW.enforcement_plan_snapshot IS NULL OR json_valid(NEW.enforcement_plan_snapshot)=0);
519
- SELECT RAISE(ABORT, 'LEASE_DISPATCH_READY_REQUIRES_DECISION')
520
- WHERE NEW.state='DISPATCH_READY' AND NOT EXISTS (
521
- SELECT 1 FROM capability_decisions d
522
- WHERE d.decision_id = NEW.capability_decision_id
523
- AND d.decision='GRANTED' AND d.enforcement_status='ENFORCED'
524
- AND d.enforcement_evidence_json IS NOT NULL
525
- );
526
- SELECT RAISE(ABORT, 'LEASE_RELEASED_REQUIRES_EVIDENCE')
527
- WHERE NEW.state='RELEASED' AND (NEW.release_evidence_json IS NULL OR NEW.released_at IS NULL);
528
- END;
529
- CREATE TRIGGER IF NOT EXISTS lease_insert_state_guard
530
- BEFORE INSERT ON execution_leases
531
- WHEN NEW.state <> 'ACQUIRED'
532
- BEGIN SELECT RAISE(ABORT, 'LEASE_INSERT_MUST_BE_ACQUIRED'); END;
533
- CREATE TRIGGER IF NOT EXISTS lease_no_delete
534
- BEFORE DELETE ON execution_leases
535
- BEGIN SELECT RAISE(ABORT, 'LEASE_NO_DELETE'); END;
536
-
537
- CREATE TABLE IF NOT EXISTS capability_decisions (
538
- decision_id TEXT PRIMARY KEY,
539
- kingdom_id TEXT NOT NULL,
540
- task_id TEXT NOT NULL,
541
- worker_binding_id TEXT,
542
- supervisor_binding_id TEXT,
543
- requirement_snapshot TEXT,
544
- ceiling_snapshot TEXT,
545
- proposed_grant_snapshot TEXT,
546
- scope_snapshot TEXT,
547
- effective_snapshot TEXT,
548
- decision TEXT NOT NULL,
549
- enforcement_status TEXT NOT NULL,
550
- enforcement_evidence_json TEXT,
551
- requirement_coverage TEXT NOT NULL DEFAULT 'NONE'
552
- CHECK(requirement_coverage IN ('FULL','PARTIAL','NONE')),
553
- reason_code TEXT,
554
- execution_id TEXT,
555
- created_at TEXT NOT NULL,
556
- CHECK (
557
- (decision = 'GRANTED' AND enforcement_status = 'ENFORCED')
558
- OR
559
- (decision = 'DENIED' AND enforcement_status IN
560
- ('NOT_ATTEMPTED','UNAVAILABLE','FAILED'))
561
- ),
562
- CHECK (decision <> 'GRANTED' OR
563
- (enforcement_evidence_json IS NOT NULL AND json_valid(enforcement_evidence_json)=1))
564
- );
565
- CREATE UNIQUE INDEX IF NOT EXISTS capability_decision_execution_uk
566
- ON capability_decisions(execution_id) WHERE execution_id IS NOT NULL;
567
- CREATE TRIGGER IF NOT EXISTS capability_decision_immutable
568
- BEFORE UPDATE OF kingdom_id, task_id, worker_binding_id, supervisor_binding_id,
569
- requirement_snapshot, ceiling_snapshot, proposed_grant_snapshot,
570
- scope_snapshot, effective_snapshot, decision, enforcement_status,
571
- enforcement_evidence_json, requirement_coverage, reason_code, created_at
572
- ON capability_decisions
573
- BEGIN SELECT RAISE(ABORT, 'CAPABILITY_DECISION_IMMUTABLE'); END;
574
- CREATE TRIGGER IF NOT EXISTS capability_decision_execution_bind
575
- BEFORE UPDATE OF execution_id
576
- ON capability_decisions
577
- WHEN NEW.execution_id IS NOT NULL
578
- BEGIN
579
- SELECT RAISE(ABORT, 'CAPABILITY_DECISION_EXECUTION_ALREADY_BOUND')
580
- WHERE OLD.execution_id IS NOT NULL;
581
- SELECT RAISE(ABORT, 'CAPABILITY_DECISION_BIND_REQUIRES_GRANTED')
582
- WHERE NOT (OLD.decision='GRANTED' AND OLD.enforcement_status='ENFORCED');
583
- END;
584
- CREATE TRIGGER IF NOT EXISTS capability_decision_no_delete
585
- BEFORE DELETE ON capability_decisions
586
- BEGIN SELECT RAISE(ABORT, 'CAPABILITY_DECISION_NO_DELETE'); END;
394
+ static V4_LEDGER_SQL = `
395
+ CREATE TABLE IF NOT EXISTS session_territory_affinities (
396
+ affinity_id TEXT PRIMARY KEY,
397
+ kingdom_id TEXT NOT NULL,
398
+ worker_binding_id TEXT NOT NULL,
399
+ runtime_type TEXT NOT NULL,
400
+ runtime_instance_ref TEXT NOT NULL,
401
+ session_ref TEXT NOT NULL,
402
+ territory_id TEXT NOT NULL,
403
+ established_at TEXT NOT NULL,
404
+ retired_at TEXT,
405
+ is_current INTEGER NOT NULL DEFAULT 1,
406
+ created_at TEXT NOT NULL,
407
+ UNIQUE (runtime_type, runtime_instance_ref, session_ref),
408
+ CHECK (
409
+ (is_current = 1 AND retired_at IS NULL)
410
+ OR
411
+ (is_current = 0 AND retired_at IS NOT NULL)
412
+ )
413
+ );
414
+ CREATE UNIQUE INDEX IF NOT EXISTS affinity_one_current_per_worker
415
+ ON session_territory_affinities(kingdom_id, worker_binding_id)
416
+ WHERE is_current = 1;
417
+ CREATE TRIGGER IF NOT EXISTS affinity_identity_immutable
418
+ BEFORE UPDATE OF kingdom_id, worker_binding_id, runtime_type,
419
+ runtime_instance_ref, session_ref, territory_id,
420
+ established_at, created_at
421
+ ON session_territory_affinities
422
+ BEGIN SELECT RAISE(ABORT, 'AFFINITY_IDENTITY_IMMUTABLE'); END;
423
+ CREATE TRIGGER IF NOT EXISTS affinity_retire
424
+ BEFORE UPDATE OF is_current, retired_at
425
+ ON session_territory_affinities
426
+ BEGIN
427
+ SELECT RAISE(ABORT, 'AFFINITY_RETIRE_ONLY_JOINT')
428
+ WHERE NOT (
429
+ OLD.is_current = 1 AND OLD.retired_at IS NULL
430
+ AND NEW.is_current = 0 AND NEW.retired_at IS NOT NULL
431
+ );
432
+ SELECT RAISE(ABORT, 'AFFINITY_ALREADY_RETIRED')
433
+ WHERE OLD.retired_at IS NOT NULL;
434
+ END;
435
+ CREATE TRIGGER IF NOT EXISTS affinity_no_delete
436
+ BEFORE DELETE ON session_territory_affinities
437
+ BEGIN SELECT RAISE(ABORT, 'AFFINITY_NO_DELETE'); END;
438
+
439
+ CREATE TABLE IF NOT EXISTS execution_leases (
440
+ lease_id TEXT PRIMARY KEY,
441
+ kingdom_id TEXT NOT NULL,
442
+ worker_binding_id TEXT NOT NULL,
443
+ runtime_type TEXT NOT NULL,
444
+ runtime_instance_ref TEXT NOT NULL,
445
+ session_ref TEXT NOT NULL,
446
+ territory_id TEXT NOT NULL,
447
+ task_id TEXT NOT NULL,
448
+ attempt_no INTEGER NOT NULL,
449
+ state TEXT NOT NULL CHECK(state IN
450
+ ('ACQUIRED','PREPARING','MATERIALIZING','DISPATCH_READY',
451
+ 'EXECUTING','SETTLING','RELEASING','RECOVERING','RELEASED')),
452
+ capability_decision_id TEXT,
453
+ enforcement_plan_snapshot TEXT,
454
+ release_evidence_json TEXT,
455
+ release_reason TEXT,
456
+ acquired_at TEXT NOT NULL,
457
+ released_at TEXT,
458
+ updated_at TEXT NOT NULL,
459
+ UNIQUE (task_id, attempt_no),
460
+ UNIQUE (runtime_type, runtime_instance_ref, session_ref, task_id, attempt_no)
461
+ );
462
+ CREATE UNIQUE INDEX IF NOT EXISTS lease_one_active_per_session
463
+ ON execution_leases(runtime_type, runtime_instance_ref, session_ref)
464
+ WHERE state <> 'RELEASED';
465
+ CREATE TRIGGER IF NOT EXISTS lease_requires_matching_affinity
466
+ BEFORE INSERT ON execution_leases
467
+ BEGIN
468
+ SELECT RAISE(ABORT, 'LEASE_REQUIRES_MATCHING_CURRENT_AFFINITY')
469
+ WHERE NOT EXISTS (
470
+ SELECT 1
471
+ FROM session_territory_affinities a
472
+ JOIN tasks t ON t.task_id = NEW.task_id
473
+ WHERE a.kingdom_id = NEW.kingdom_id
474
+ AND a.worker_binding_id = NEW.worker_binding_id
475
+ AND a.runtime_type = NEW.runtime_type
476
+ AND a.runtime_instance_ref = NEW.runtime_instance_ref
477
+ AND a.session_ref = NEW.session_ref
478
+ AND a.is_current = 1 AND a.retired_at IS NULL
479
+ AND a.territory_id = NEW.territory_id
480
+ AND t.territory_id = NEW.territory_id
481
+ );
482
+ END;
483
+ CREATE TRIGGER IF NOT EXISTS lease_identity_immutable
484
+ BEFORE UPDATE OF kingdom_id, worker_binding_id, runtime_type, runtime_instance_ref,
485
+ session_ref, territory_id, task_id, attempt_no, acquired_at
486
+ ON execution_leases
487
+ BEGIN SELECT RAISE(ABORT, 'LEASE_IDENTITY_IMMUTABLE'); END;
488
+ CREATE TRIGGER IF NOT EXISTS lease_plan_once
489
+ BEFORE UPDATE OF enforcement_plan_snapshot
490
+ ON execution_leases
491
+ WHEN OLD.enforcement_plan_snapshot IS NOT NULL
492
+ BEGIN SELECT RAISE(ABORT, 'LEASE_PLAN_ALREADY_SET'); END;
493
+ CREATE TRIGGER IF NOT EXISTS lease_decision_once
494
+ BEFORE UPDATE OF capability_decision_id
495
+ ON execution_leases
496
+ WHEN OLD.capability_decision_id IS NOT NULL
497
+ BEGIN SELECT RAISE(ABORT, 'LEASE_DECISION_ALREADY_BOUND'); END;
498
+ CREATE TRIGGER IF NOT EXISTS lease_release_evidence_once
499
+ BEFORE UPDATE OF release_evidence_json, release_reason
500
+ ON execution_leases
501
+ WHEN OLD.release_evidence_json IS NOT NULL
502
+ BEGIN SELECT RAISE(ABORT, 'LEASE_RELEASE_EVIDENCE_ALREADY_SET'); END;
503
+ CREATE TRIGGER IF NOT EXISTS lease_state_guard
504
+ BEFORE UPDATE OF state ON execution_leases
505
+ WHEN NEW.state <> OLD.state
506
+ BEGIN
507
+ SELECT RAISE(ABORT, 'ILLEGAL_LEASE_TRANSITION') WHERE NOT (
508
+ (OLD.state='ACQUIRED' AND NEW.state IN ('PREPARING','RECOVERING'))
509
+ OR (OLD.state='PREPARING' AND NEW.state IN ('MATERIALIZING','RECOVERING'))
510
+ OR (OLD.state='MATERIALIZING' AND NEW.state IN ('DISPATCH_READY','RECOVERING','RELEASED'))
511
+ OR (OLD.state='DISPATCH_READY' AND NEW.state IN ('EXECUTING','RECOVERING'))
512
+ OR (OLD.state='EXECUTING' AND NEW.state IN ('SETTLING','RECOVERING'))
513
+ OR (OLD.state='SETTLING' AND NEW.state IN ('RELEASING','RECOVERING'))
514
+ OR (OLD.state='RELEASING' AND NEW.state IN ('RELEASED','RECOVERING'))
515
+ OR (OLD.state='RECOVERING' AND NEW.state IN ('RELEASED','RECOVERING'))
516
+ );
517
+ SELECT RAISE(ABORT, 'LEASE_MATERIALIZING_REQUIRES_PLAN')
518
+ WHERE NEW.state='MATERIALIZING' AND (NEW.enforcement_plan_snapshot IS NULL OR json_valid(NEW.enforcement_plan_snapshot)=0);
519
+ SELECT RAISE(ABORT, 'LEASE_DISPATCH_READY_REQUIRES_DECISION')
520
+ WHERE NEW.state='DISPATCH_READY' AND NOT EXISTS (
521
+ SELECT 1 FROM capability_decisions d
522
+ WHERE d.decision_id = NEW.capability_decision_id
523
+ AND d.decision='GRANTED' AND d.enforcement_status='ENFORCED'
524
+ AND d.enforcement_evidence_json IS NOT NULL
525
+ );
526
+ SELECT RAISE(ABORT, 'LEASE_RELEASED_REQUIRES_EVIDENCE')
527
+ WHERE NEW.state='RELEASED' AND (NEW.release_evidence_json IS NULL OR NEW.released_at IS NULL);
528
+ END;
529
+ CREATE TRIGGER IF NOT EXISTS lease_insert_state_guard
530
+ BEFORE INSERT ON execution_leases
531
+ WHEN NEW.state <> 'ACQUIRED'
532
+ BEGIN SELECT RAISE(ABORT, 'LEASE_INSERT_MUST_BE_ACQUIRED'); END;
533
+ CREATE TRIGGER IF NOT EXISTS lease_no_delete
534
+ BEFORE DELETE ON execution_leases
535
+ BEGIN SELECT RAISE(ABORT, 'LEASE_NO_DELETE'); END;
536
+
537
+ CREATE TABLE IF NOT EXISTS capability_decisions (
538
+ decision_id TEXT PRIMARY KEY,
539
+ kingdom_id TEXT NOT NULL,
540
+ task_id TEXT NOT NULL,
541
+ worker_binding_id TEXT,
542
+ supervisor_binding_id TEXT,
543
+ requirement_snapshot TEXT,
544
+ ceiling_snapshot TEXT,
545
+ proposed_grant_snapshot TEXT,
546
+ scope_snapshot TEXT,
547
+ effective_snapshot TEXT,
548
+ decision TEXT NOT NULL,
549
+ enforcement_status TEXT NOT NULL,
550
+ enforcement_evidence_json TEXT,
551
+ requirement_coverage TEXT NOT NULL DEFAULT 'NONE'
552
+ CHECK(requirement_coverage IN ('FULL','PARTIAL','NONE')),
553
+ reason_code TEXT,
554
+ execution_id TEXT,
555
+ created_at TEXT NOT NULL,
556
+ CHECK (
557
+ (decision = 'GRANTED' AND enforcement_status = 'ENFORCED')
558
+ OR
559
+ (decision = 'DENIED' AND enforcement_status IN
560
+ ('NOT_ATTEMPTED','UNAVAILABLE','FAILED'))
561
+ ),
562
+ CHECK (decision <> 'GRANTED' OR
563
+ (enforcement_evidence_json IS NOT NULL AND json_valid(enforcement_evidence_json)=1))
564
+ );
565
+ CREATE UNIQUE INDEX IF NOT EXISTS capability_decision_execution_uk
566
+ ON capability_decisions(execution_id) WHERE execution_id IS NOT NULL;
567
+ CREATE TRIGGER IF NOT EXISTS capability_decision_immutable
568
+ BEFORE UPDATE OF kingdom_id, task_id, worker_binding_id, supervisor_binding_id,
569
+ requirement_snapshot, ceiling_snapshot, proposed_grant_snapshot,
570
+ scope_snapshot, effective_snapshot, decision, enforcement_status,
571
+ enforcement_evidence_json, requirement_coverage, reason_code, created_at
572
+ ON capability_decisions
573
+ BEGIN SELECT RAISE(ABORT, 'CAPABILITY_DECISION_IMMUTABLE'); END;
574
+ CREATE TRIGGER IF NOT EXISTS capability_decision_execution_bind
575
+ BEFORE UPDATE OF execution_id
576
+ ON capability_decisions
577
+ WHEN NEW.execution_id IS NOT NULL
578
+ BEGIN
579
+ SELECT RAISE(ABORT, 'CAPABILITY_DECISION_EXECUTION_ALREADY_BOUND')
580
+ WHERE OLD.execution_id IS NOT NULL;
581
+ SELECT RAISE(ABORT, 'CAPABILITY_DECISION_BIND_REQUIRES_GRANTED')
582
+ WHERE NOT (OLD.decision='GRANTED' AND OLD.enforcement_status='ENFORCED');
583
+ END;
584
+ CREATE TRIGGER IF NOT EXISTS capability_decision_no_delete
585
+ BEFORE DELETE ON capability_decisions
586
+ BEGIN SELECT RAISE(ABORT, 'CAPABILITY_DECISION_NO_DELETE'); END;
587
587
  `;
588
588
  /**
589
589
  * executions 重建形态(暂存名 executions_v4,迁移后 RENAME 回 executions)。
590
590
  * = 旧 executions 全部列(含 v2 证据列)+ 新增 3 列(v6 §8.1)。
591
591
  */
592
- static V4_EXECUTIONS_STAGING_SQL = `
593
- CREATE TABLE IF NOT EXISTS executions_v4 (
594
- execution_id TEXT PRIMARY KEY,
595
- task_id TEXT NOT NULL,
596
- attempt_no INTEGER NOT NULL,
597
- worker_binding_id TEXT,
598
- session_id TEXT,
599
- state TEXT NOT NULL CHECK(state IN
600
- ('STARTING','RUNNING','PAUSED','RECOVERING','COMPLETED','FAILED','ABORTED')),
601
- detail TEXT,
602
- started_at TEXT NOT NULL,
603
- heartbeat_at TEXT,
604
- ended_at TEXT,
605
- pause_requested_at TEXT,
606
- executor_kind TEXT,
607
- provider TEXT,
608
- provider_source TEXT,
609
- requested_model TEXT,
610
- resolved_model TEXT,
611
- model_source TEXT,
612
- execution_profile_json TEXT,
613
- execution_contract TEXT NOT NULL DEFAULT 'LEGACY_COMPAT'
614
- CHECK(execution_contract IN ('LEGACY_COMPAT','GOVERNED_PERSISTENT')),
615
- lease_id TEXT,
616
- capability_decision_id TEXT,
617
- UNIQUE (task_id, attempt_no),
618
- CHECK (
619
- execution_contract = 'LEGACY_COMPAT'
620
- OR (
621
- execution_contract = 'GOVERNED_PERSISTENT'
622
- AND lease_id IS NOT NULL
623
- AND capability_decision_id IS NOT NULL
624
- )
625
- ),
626
- FOREIGN KEY (lease_id) REFERENCES execution_leases(lease_id),
627
- FOREIGN KEY (capability_decision_id) REFERENCES capability_decisions(decision_id)
628
- );
592
+ static V4_EXECUTIONS_STAGING_SQL = `
593
+ CREATE TABLE IF NOT EXISTS executions_v4 (
594
+ execution_id TEXT PRIMARY KEY,
595
+ task_id TEXT NOT NULL,
596
+ attempt_no INTEGER NOT NULL,
597
+ worker_binding_id TEXT,
598
+ session_id TEXT,
599
+ state TEXT NOT NULL CHECK(state IN
600
+ ('STARTING','RUNNING','PAUSED','RECOVERING','COMPLETED','FAILED','ABORTED')),
601
+ detail TEXT,
602
+ started_at TEXT NOT NULL,
603
+ heartbeat_at TEXT,
604
+ ended_at TEXT,
605
+ pause_requested_at TEXT,
606
+ executor_kind TEXT,
607
+ provider TEXT,
608
+ provider_source TEXT,
609
+ requested_model TEXT,
610
+ resolved_model TEXT,
611
+ model_source TEXT,
612
+ execution_profile_json TEXT,
613
+ execution_contract TEXT NOT NULL DEFAULT 'LEGACY_COMPAT'
614
+ CHECK(execution_contract IN ('LEGACY_COMPAT','GOVERNED_PERSISTENT')),
615
+ lease_id TEXT,
616
+ capability_decision_id TEXT,
617
+ UNIQUE (task_id, attempt_no),
618
+ CHECK (
619
+ execution_contract = 'LEGACY_COMPAT'
620
+ OR (
621
+ execution_contract = 'GOVERNED_PERSISTENT'
622
+ AND lease_id IS NOT NULL
623
+ AND capability_decision_id IS NOT NULL
624
+ )
625
+ ),
626
+ FOREIGN KEY (lease_id) REFERENCES execution_leases(lease_id),
627
+ FOREIGN KEY (capability_decision_id) REFERENCES capability_decisions(decision_id)
628
+ );
629
629
  `;
630
630
  /** executions 重建后的 trigger(RENAME 回 executions 之后创建;不依赖 RENAME 自动改写)。 */
631
- static V4_EXECUTIONS_TRIGGERS_SQL = `
632
- CREATE TRIGGER IF NOT EXISTS execution_governed_consistency
633
- BEFORE INSERT ON executions
634
- WHEN NEW.execution_contract = 'GOVERNED_PERSISTENT'
635
- BEGIN
636
- SELECT RAISE(ABORT, 'EXECUTION_GOVERNED_REQUIRES_MATCHING_LEASE')
637
- WHERE NOT EXISTS (
638
- SELECT 1
639
- FROM execution_leases l
640
- JOIN capability_decisions d ON d.decision_id = NEW.capability_decision_id
641
- WHERE l.lease_id = NEW.lease_id
642
- AND l.task_id = NEW.task_id
643
- AND l.attempt_no = NEW.attempt_no
644
- AND l.capability_decision_id = NEW.capability_decision_id
645
- AND l.kingdom_id = d.kingdom_id
646
- AND d.task_id = NEW.task_id
647
- AND d.decision='GRANTED' AND d.enforcement_status='ENFORCED'
648
- AND (d.execution_id IS NULL OR d.execution_id = NEW.execution_id)
649
- );
650
- END;
651
- CREATE TRIGGER IF NOT EXISTS execution_contract_immutable
652
- BEFORE UPDATE OF execution_contract ON executions
653
- BEGIN SELECT RAISE(ABORT, 'EXECUTION_CONTRACT_IMMUTABLE'); END;
631
+ static V4_EXECUTIONS_TRIGGERS_SQL = `
632
+ CREATE TRIGGER IF NOT EXISTS execution_governed_consistency
633
+ BEFORE INSERT ON executions
634
+ WHEN NEW.execution_contract = 'GOVERNED_PERSISTENT'
635
+ BEGIN
636
+ SELECT RAISE(ABORT, 'EXECUTION_GOVERNED_REQUIRES_MATCHING_LEASE')
637
+ WHERE NOT EXISTS (
638
+ SELECT 1
639
+ FROM execution_leases l
640
+ JOIN capability_decisions d ON d.decision_id = NEW.capability_decision_id
641
+ WHERE l.lease_id = NEW.lease_id
642
+ AND l.task_id = NEW.task_id
643
+ AND l.attempt_no = NEW.attempt_no
644
+ AND l.capability_decision_id = NEW.capability_decision_id
645
+ AND l.kingdom_id = d.kingdom_id
646
+ AND d.task_id = NEW.task_id
647
+ AND d.decision='GRANTED' AND d.enforcement_status='ENFORCED'
648
+ AND (d.execution_id IS NULL OR d.execution_id = NEW.execution_id)
649
+ );
650
+ END;
651
+ CREATE TRIGGER IF NOT EXISTS execution_contract_immutable
652
+ BEFORE UPDATE OF execution_contract ON executions
653
+ BEGIN SELECT RAISE(ABORT, 'EXECUTION_CONTRACT_IMMUTABLE'); END;
654
654
  `;
655
655
  /** Dispatch Ledger(executions 重建完成后创建;FK 直接指向最终名 executions)。 */
656
- static V4_DISPATCH_SQL = `
657
- CREATE TABLE IF NOT EXISTS dispatch_records (
658
- dispatch_id TEXT PRIMARY KEY,
659
- kingdom_id TEXT NOT NULL,
660
- lease_id TEXT NOT NULL,
661
- execution_id TEXT NOT NULL,
662
- task_id TEXT NOT NULL,
663
- attempt_no INTEGER NOT NULL,
664
- runtime_type TEXT NOT NULL,
665
- runtime_instance_ref TEXT NOT NULL,
666
- session_ref TEXT NOT NULL,
667
- state TEXT NOT NULL CHECK(state IN
668
- ('INTENDED','DISPATCHED','RECEIVED','CORRELATED','TERMINAL','FAILED','RECOVERING')),
669
- dispatch_request_snapshot TEXT NOT NULL,
670
- dispatch_input_ref_json TEXT NOT NULL,
671
- dispatch_payload_hash TEXT NOT NULL,
672
- runtime_dispatch_ref TEXT,
673
- runtime_execution_ref TEXT,
674
- receipt_json TEXT,
675
- terminal_evidence_json TEXT,
676
- output_ref_json TEXT,
677
- dispatched_at TEXT,
678
- receipt_at TEXT,
679
- terminal_at TEXT,
680
- created_at TEXT NOT NULL,
681
- updated_at TEXT NOT NULL,
682
- FOREIGN KEY (lease_id) REFERENCES execution_leases(lease_id),
683
- FOREIGN KEY (execution_id) REFERENCES executions(execution_id),
684
- UNIQUE (lease_id),
685
- UNIQUE (execution_id)
686
- );
687
- CREATE TRIGGER IF NOT EXISTS dispatch_request_immutable
688
- BEFORE UPDATE OF dispatch_id, kingdom_id, lease_id, execution_id, task_id, attempt_no,
689
- runtime_type, runtime_instance_ref, session_ref,
690
- dispatch_request_snapshot, dispatch_input_ref_json, dispatch_payload_hash,
691
- created_at
692
- ON dispatch_records
693
- BEGIN SELECT RAISE(ABORT, 'DISPATCH_REQUEST_IMMUTABLE'); END;
694
- CREATE TRIGGER IF NOT EXISTS dispatch_requires_ready_lease
695
- BEFORE INSERT ON dispatch_records
696
- BEGIN
697
- SELECT RAISE(ABORT, 'DISPATCH_REQUIRES_MATCHING_READY_LEASE')
698
- WHERE NOT EXISTS (
699
- SELECT 1
700
- FROM execution_leases l
701
- JOIN executions e ON e.execution_id = NEW.execution_id
702
- WHERE l.lease_id = NEW.lease_id
703
- AND l.state = 'DISPATCH_READY'
704
- AND l.kingdom_id = NEW.kingdom_id
705
- AND l.task_id = NEW.task_id
706
- AND l.attempt_no = NEW.attempt_no
707
- AND l.runtime_type = NEW.runtime_type
708
- AND l.runtime_instance_ref = NEW.runtime_instance_ref
709
- AND l.session_ref = NEW.session_ref
710
- AND e.task_id = NEW.task_id
711
- AND e.attempt_no = NEW.attempt_no
712
- );
713
- END;
714
- CREATE TRIGGER IF NOT EXISTS dispatch_state_guard
715
- BEFORE UPDATE OF state ON dispatch_records
716
- WHEN NEW.state <> OLD.state
717
- BEGIN
718
- SELECT RAISE(ABORT, 'ILLEGAL_DISPATCH_TRANSITION') WHERE NOT (
719
- (OLD.state='INTENDED' AND NEW.state IN ('DISPATCHED','FAILED','RECOVERING'))
720
- OR (OLD.state='DISPATCHED' AND NEW.state IN ('RECEIVED','RECOVERING'))
721
- OR (OLD.state='RECEIVED' AND NEW.state IN ('CORRELATED','RECOVERING'))
722
- OR (OLD.state='CORRELATED' AND NEW.state IN ('TERMINAL','RECOVERING'))
723
- OR (OLD.state='RECOVERING' AND NEW.state IN ('TERMINAL','FAILED','RECOVERING'))
724
- );
725
- SELECT RAISE(ABORT, 'DISPATCH_RECEIVED_REQUIRES_REF_RECEIPT')
726
- WHERE NEW.state='RECEIVED' AND (NEW.runtime_dispatch_ref IS NULL OR NEW.receipt_json IS NULL OR NEW.receipt_at IS NULL);
727
- SELECT RAISE(ABORT, 'DISPATCH_CORRELATED_REQUIRES_EXEC_REF')
728
- WHERE NEW.state='CORRELATED' AND NEW.runtime_execution_ref IS NULL;
729
- SELECT RAISE(ABORT, 'DISPATCH_TERMINAL_REQUIRES_EVIDENCE')
730
- WHERE NEW.state='TERMINAL' AND (NEW.terminal_evidence_json IS NULL OR NEW.terminal_at IS NULL);
731
- END;
732
- CREATE TRIGGER IF NOT EXISTS dispatch_insert_state_guard
733
- BEFORE INSERT ON dispatch_records
734
- WHEN NEW.state <> 'INTENDED'
735
- BEGIN SELECT RAISE(ABORT, 'DISPATCH_INSERT_MUST_BE_INTENDED'); END;
736
- CREATE TRIGGER IF NOT EXISTS dispatch_no_delete
737
- BEFORE DELETE ON dispatch_records
738
- BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
656
+ static V4_DISPATCH_SQL = `
657
+ CREATE TABLE IF NOT EXISTS dispatch_records (
658
+ dispatch_id TEXT PRIMARY KEY,
659
+ kingdom_id TEXT NOT NULL,
660
+ lease_id TEXT NOT NULL,
661
+ execution_id TEXT NOT NULL,
662
+ task_id TEXT NOT NULL,
663
+ attempt_no INTEGER NOT NULL,
664
+ runtime_type TEXT NOT NULL,
665
+ runtime_instance_ref TEXT NOT NULL,
666
+ session_ref TEXT NOT NULL,
667
+ state TEXT NOT NULL CHECK(state IN
668
+ ('INTENDED','DISPATCHED','RECEIVED','CORRELATED','TERMINAL','FAILED','RECOVERING')),
669
+ dispatch_request_snapshot TEXT NOT NULL,
670
+ dispatch_input_ref_json TEXT NOT NULL,
671
+ dispatch_payload_hash TEXT NOT NULL,
672
+ runtime_dispatch_ref TEXT,
673
+ runtime_execution_ref TEXT,
674
+ receipt_json TEXT,
675
+ terminal_evidence_json TEXT,
676
+ output_ref_json TEXT,
677
+ dispatched_at TEXT,
678
+ receipt_at TEXT,
679
+ terminal_at TEXT,
680
+ created_at TEXT NOT NULL,
681
+ updated_at TEXT NOT NULL,
682
+ FOREIGN KEY (lease_id) REFERENCES execution_leases(lease_id),
683
+ FOREIGN KEY (execution_id) REFERENCES executions(execution_id),
684
+ UNIQUE (lease_id),
685
+ UNIQUE (execution_id)
686
+ );
687
+ CREATE TRIGGER IF NOT EXISTS dispatch_request_immutable
688
+ BEFORE UPDATE OF dispatch_id, kingdom_id, lease_id, execution_id, task_id, attempt_no,
689
+ runtime_type, runtime_instance_ref, session_ref,
690
+ dispatch_request_snapshot, dispatch_input_ref_json, dispatch_payload_hash,
691
+ created_at
692
+ ON dispatch_records
693
+ BEGIN SELECT RAISE(ABORT, 'DISPATCH_REQUEST_IMMUTABLE'); END;
694
+ CREATE TRIGGER IF NOT EXISTS dispatch_requires_ready_lease
695
+ BEFORE INSERT ON dispatch_records
696
+ BEGIN
697
+ SELECT RAISE(ABORT, 'DISPATCH_REQUIRES_MATCHING_READY_LEASE')
698
+ WHERE NOT EXISTS (
699
+ SELECT 1
700
+ FROM execution_leases l
701
+ JOIN executions e ON e.execution_id = NEW.execution_id
702
+ WHERE l.lease_id = NEW.lease_id
703
+ AND l.state = 'DISPATCH_READY'
704
+ AND l.kingdom_id = NEW.kingdom_id
705
+ AND l.task_id = NEW.task_id
706
+ AND l.attempt_no = NEW.attempt_no
707
+ AND l.runtime_type = NEW.runtime_type
708
+ AND l.runtime_instance_ref = NEW.runtime_instance_ref
709
+ AND l.session_ref = NEW.session_ref
710
+ AND e.task_id = NEW.task_id
711
+ AND e.attempt_no = NEW.attempt_no
712
+ );
713
+ END;
714
+ CREATE TRIGGER IF NOT EXISTS dispatch_state_guard
715
+ BEFORE UPDATE OF state ON dispatch_records
716
+ WHEN NEW.state <> OLD.state
717
+ BEGIN
718
+ SELECT RAISE(ABORT, 'ILLEGAL_DISPATCH_TRANSITION') WHERE NOT (
719
+ (OLD.state='INTENDED' AND NEW.state IN ('DISPATCHED','FAILED','RECOVERING'))
720
+ OR (OLD.state='DISPATCHED' AND NEW.state IN ('RECEIVED','RECOVERING'))
721
+ OR (OLD.state='RECEIVED' AND NEW.state IN ('CORRELATED','RECOVERING'))
722
+ OR (OLD.state='CORRELATED' AND NEW.state IN ('TERMINAL','RECOVERING'))
723
+ OR (OLD.state='RECOVERING' AND NEW.state IN ('TERMINAL','FAILED','RECOVERING'))
724
+ );
725
+ SELECT RAISE(ABORT, 'DISPATCH_RECEIVED_REQUIRES_REF_RECEIPT')
726
+ WHERE NEW.state='RECEIVED' AND (NEW.runtime_dispatch_ref IS NULL OR NEW.receipt_json IS NULL OR NEW.receipt_at IS NULL);
727
+ SELECT RAISE(ABORT, 'DISPATCH_CORRELATED_REQUIRES_EXEC_REF')
728
+ WHERE NEW.state='CORRELATED' AND NEW.runtime_execution_ref IS NULL;
729
+ SELECT RAISE(ABORT, 'DISPATCH_TERMINAL_REQUIRES_EVIDENCE')
730
+ WHERE NEW.state='TERMINAL' AND (NEW.terminal_evidence_json IS NULL OR NEW.terminal_at IS NULL);
731
+ END;
732
+ CREATE TRIGGER IF NOT EXISTS dispatch_insert_state_guard
733
+ BEFORE INSERT ON dispatch_records
734
+ WHEN NEW.state <> 'INTENDED'
735
+ BEGIN SELECT RAISE(ABORT, 'DISPATCH_INSERT_MUST_BE_INTENDED'); END;
736
+ CREATE TRIGGER IF NOT EXISTS dispatch_no_delete
737
+ BEFORE DELETE ON dispatch_records
738
+ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
739
739
  `;
740
740
  /** v4 对象是否已存在(execution_leases 表是 v4 迁移完成的地标)。 */
741
741
  hasV4Objects() {
@@ -776,20 +776,20 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
776
776
  this.db.exec(KingdomStore.V4_LEDGER_SQL);
777
777
  // c. executions 重建:暂存表 → 复制(LEGACY_COMPAT backfill)→ 行数校验
778
778
  this.db.exec(KingdomStore.V4_EXECUTIONS_STAGING_SQL);
779
- this.db.exec(`
780
- INSERT INTO executions_v4
781
- (execution_id, task_id, attempt_no, worker_binding_id, session_id, state,
782
- detail, started_at, heartbeat_at, ended_at, pause_requested_at,
783
- executor_kind, provider, provider_source, requested_model, resolved_model,
784
- model_source, execution_profile_json,
785
- execution_contract, lease_id, capability_decision_id)
786
- SELECT
787
- execution_id, task_id, attempt_no, worker_binding_id, session_id, state,
788
- detail, started_at, heartbeat_at, ended_at, pause_requested_at,
789
- executor_kind, provider, provider_source, requested_model, resolved_model,
790
- model_source, execution_profile_json,
791
- 'LEGACY_COMPAT', NULL, NULL
792
- FROM executions
779
+ this.db.exec(`
780
+ INSERT INTO executions_v4
781
+ (execution_id, task_id, attempt_no, worker_binding_id, session_id, state,
782
+ detail, started_at, heartbeat_at, ended_at, pause_requested_at,
783
+ executor_kind, provider, provider_source, requested_model, resolved_model,
784
+ model_source, execution_profile_json,
785
+ execution_contract, lease_id, capability_decision_id)
786
+ SELECT
787
+ execution_id, task_id, attempt_no, worker_binding_id, session_id, state,
788
+ detail, started_at, heartbeat_at, ended_at, pause_requested_at,
789
+ executor_kind, provider, provider_source, requested_model, resolved_model,
790
+ model_source, execution_profile_json,
791
+ 'LEGACY_COMPAT', NULL, NULL
792
+ FROM executions
793
793
  `);
794
794
  const oldCount = this.db.prepare('SELECT COUNT(*) AS n FROM executions').get();
795
795
  const newCount = this.db.prepare('SELECT COUNT(*) AS n FROM executions_v4').get();
@@ -900,7 +900,7 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
900
900
  ...row,
901
901
  };
902
902
  this.db
903
- .prepare(`INSERT INTO kingdoms (kingdom_id, name, created_at, owner_id, owner_name, schema_version)
903
+ .prepare(`INSERT INTO kingdoms (kingdom_id, name, created_at, owner_id, owner_name, schema_version)
904
904
  VALUES (?, ?, ?, ?, ?, ?)`)
905
905
  .run(full.kingdom_id, full.name, full.created_at, full.owner_id, full.owner_name, full.schema_version);
906
906
  return full;
@@ -926,8 +926,8 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
926
926
  }
927
927
  insertTerritory(row) {
928
928
  this.db
929
- .prepare(`INSERT INTO territories
930
- (territory_id, kingdom_id, name, workspace_path, summary, supervisor_binding_id, status, created_at)
929
+ .prepare(`INSERT INTO territories
930
+ (territory_id, kingdom_id, name, workspace_path, summary, supervisor_binding_id, status, created_at)
931
931
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
932
932
  .run(row.territory_id, row.kingdom_id, row.name, row.workspace_path, row.summary, row.supervisor_binding_id, row.status, row.created_at);
933
933
  return row;
@@ -963,16 +963,16 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
963
963
  }
964
964
  /** Owner topology guards include RECOVERING and unknown future nonterminal states. */
965
965
  listUnsettledOwnerExecutions(kingdomId) {
966
- return this.db.prepare(`SELECT e.* FROM executions e JOIN tasks t ON t.task_id = e.task_id
967
- JOIN territories te ON te.territory_id = t.territory_id WHERE te.kingdom_id = ?
966
+ return this.db.prepare(`SELECT e.* FROM executions e JOIN tasks t ON t.task_id = e.task_id
967
+ JOIN territories te ON te.territory_id = t.territory_id WHERE te.kingdom_id = ?
968
968
  AND e.state NOT IN ('COMPLETED', 'FAILED', 'ABORTED') ORDER BY e.started_at`).all(kingdomId);
969
969
  }
970
970
  // ── task_assignments(v0.7.0 M2-B Assignment Ledger)──────────
971
971
  insertTaskAssignment(row) {
972
972
  this.db
973
- .prepare(`INSERT INTO task_assignments
974
- (assignment_id, task_id, territory_id, worker_binding_id, assigned_by, assigned_at,
975
- ended_at, end_reason, previous_assignment_id, handoff_reason, created_at)
973
+ .prepare(`INSERT INTO task_assignments
974
+ (assignment_id, task_id, territory_id, worker_binding_id, assigned_by, assigned_at,
975
+ ended_at, end_reason, previous_assignment_id, handoff_reason, created_at)
976
976
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
977
977
  .run(row.assignment_id, row.task_id, row.territory_id, row.worker_binding_id, row.assigned_by, row.assigned_at, row.ended_at, row.end_reason, row.previous_assignment_id, row.handoff_reason, row.created_at);
978
978
  return row;
@@ -1041,10 +1041,10 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1041
1041
  }
1042
1042
  insertBinding(row) {
1043
1043
  this.db
1044
- .prepare(`INSERT INTO role_bindings
1045
- (binding_id, kingdom_id, role_type, role_name, runtime_type,
1046
- session_id, model_name, agent_name, session_meta, execution_profile_json,
1047
- status, retired_at, retired_reason, principal_id, created_at, updated_at)
1044
+ .prepare(`INSERT INTO role_bindings
1045
+ (binding_id, kingdom_id, role_type, role_name, runtime_type,
1046
+ session_id, model_name, agent_name, session_meta, execution_profile_json,
1047
+ status, retired_at, retired_reason, principal_id, created_at, updated_at)
1048
1048
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1049
1049
  .run(row.binding_id, row.kingdom_id, row.role_type, row.role_name, row.runtime_type, row.session_id, row.model_name ?? null, row.agent_name ?? null, row.session_meta ?? null, row.execution_profile_json ?? null, row.status ?? 'ACTIVE', row.retired_at ?? null, row.retired_reason ?? null, row.principal_id, row.created_at, row.updated_at);
1050
1050
  return row;
@@ -1113,8 +1113,8 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1113
1113
  */
1114
1114
  listDispatchesForTaskAttempt(taskId, attemptNo) {
1115
1115
  return this.db
1116
- .prepare(`SELECT * FROM dispatch_records
1117
- WHERE task_id = ? AND attempt_no = ?
1116
+ .prepare(`SELECT * FROM dispatch_records
1117
+ WHERE task_id = ? AND attempt_no = ?
1118
1118
  ORDER BY created_at ASC`)
1119
1119
  .all(taskId, attemptNo);
1120
1120
  }
@@ -1134,11 +1134,11 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1134
1134
  || dispatch.attempt_no !== attemptNo)
1135
1135
  return null;
1136
1136
  const rows = this.db
1137
- .prepare(`SELECT * FROM events
1138
- WHERE kingdom_id = ?
1139
- AND event_type = 'DISPATCH_TERMINAL_INTEGRITY_INCIDENT'
1140
- AND target_type = 'dispatch'
1141
- AND target_id = ?
1137
+ .prepare(`SELECT * FROM events
1138
+ WHERE kingdom_id = ?
1139
+ AND event_type = 'DISPATCH_TERMINAL_INTEGRITY_INCIDENT'
1140
+ AND target_type = 'dispatch'
1141
+ AND target_id = ?
1142
1142
  ORDER BY seq ASC`)
1143
1143
  .all(kingdomId, dispatchId);
1144
1144
  for (const row of rows) {
@@ -1170,13 +1170,13 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1170
1170
  }
1171
1171
  /** Observations and future admission/window policy cannot change an in-flight Runner relation. */
1172
1172
  runnerContextRevision(kingdomId, scope) {
1173
- const rows = this.db.prepare(`SELECT * FROM events
1174
- WHERE kingdom_id = ? AND event_type NOT IN (
1175
- 'RUNTIME_ROLE_USAGE_OBSERVED', 'RUNTIME_PROMPT_COST_OBSERVED', 'DISPATCH_USAGE_OBSERVED',
1176
- 'BUDGET_POLICY_UPDATED', 'BUDGET_ADMISSION_RESERVED', 'BUDGET_ADMISSION_BOUND', 'BUDGET_ADMISSION_CANCELLED',
1177
- 'OWNER_DECISION_CREATED', 'OWNER_DECISION_REVOKED', 'OWNER_DECISION_CONSUMED'
1178
- ) AND NOT (event_type = 'OWNER_OPERATION_APPLIED' AND COALESCE(
1179
- CASE WHEN json_valid(payload_json) THEN json_extract(payload_json, '$.action') = 'budget.policy' ELSE 0 END, 0
1173
+ const rows = this.db.prepare(`SELECT * FROM events
1174
+ WHERE kingdom_id = ? AND event_type NOT IN (
1175
+ 'RUNTIME_ROLE_USAGE_OBSERVED', 'RUNTIME_PROMPT_COST_OBSERVED', 'DISPATCH_USAGE_OBSERVED',
1176
+ 'BUDGET_POLICY_UPDATED', 'BUDGET_ADMISSION_RESERVED', 'BUDGET_ADMISSION_BOUND', 'BUDGET_ADMISSION_CANCELLED',
1177
+ 'OWNER_DECISION_CREATED', 'OWNER_DECISION_REVOKED', 'OWNER_DECISION_CONSUMED'
1178
+ ) AND NOT (event_type = 'OWNER_OPERATION_APPLIED' AND COALESCE(
1179
+ CASE WHEN json_valid(payload_json) THEN json_extract(payload_json, '$.action') = 'budget.policy' ELSE 0 END, 0
1180
1180
  )) ORDER BY seq DESC`).all(kingdomId);
1181
1181
  if (!scope)
1182
1182
  return rows[0]?.seq ?? 0;
@@ -1305,8 +1305,8 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1305
1305
  .get();
1306
1306
  const seq = (next?.n ?? 0) + 1;
1307
1307
  this.db
1308
- .prepare(`INSERT INTO events
1309
- (event_id, kingdom_id, event_type, actor_role, actor_id, target_type, target_id, payload_json, created_at, seq)
1308
+ .prepare(`INSERT INTO events
1309
+ (event_id, kingdom_id, event_type, actor_role, actor_id, target_type, target_id, payload_json, created_at, seq)
1310
1310
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1311
1311
  .run(row.event_id, row.kingdom_id, row.event_type, row.actor_role, row.actor_id, row.target_type, row.target_id, row.payload_json, row.created_at, seq);
1312
1312
  if (!outer)
@@ -1342,9 +1342,9 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1342
1342
  params.push(filter.status);
1343
1343
  }
1344
1344
  return this.db
1345
- .prepare(`SELECT t.* FROM tasks t
1346
- JOIN territories te ON te.territory_id = t.territory_id
1347
- WHERE ${clauses.join(' AND ')}
1345
+ .prepare(`SELECT t.* FROM tasks t
1346
+ JOIN territories te ON te.territory_id = t.territory_id
1347
+ WHERE ${clauses.join(' AND ')}
1348
1348
  ORDER BY t.created_at`)
1349
1349
  .all(...params);
1350
1350
  }
@@ -1359,9 +1359,9 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1359
1359
  const columns = hasRequirementColumn ? ', capability_requirement_json' : '';
1360
1360
  const values = hasRequirementColumn ? ', ?' : '';
1361
1361
  this.db
1362
- .prepare(`INSERT INTO tasks
1363
- (task_id, territory_id, parent_task_id, title, description, assigned_binding_id,
1364
- status, acceptance_criteria, result_summary, created_at, updated_at${columns})
1362
+ .prepare(`INSERT INTO tasks
1363
+ (task_id, territory_id, parent_task_id, title, description, assigned_binding_id,
1364
+ status, acceptance_criteria, result_summary, created_at, updated_at${columns})
1365
1365
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${values})`)
1366
1366
  .run(row.task_id, row.territory_id, row.parent_task_id, row.title, row.description, row.assigned_binding_id, row.status, row.acceptance_criteria, row.result_summary, row.created_at, row.updated_at, ...(hasRequirementColumn ? [row.capability_requirement_json ?? null] : []));
1367
1367
  return row;
@@ -1387,11 +1387,22 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1387
1387
  : patch.assigned_binding_id;
1388
1388
  const summary = patch.result_summary === undefined ? task.result_summary : patch.result_summary;
1389
1389
  this.db
1390
- .prepare(`UPDATE tasks SET status = ?, assigned_binding_id = ?, result_summary = ?, updated_at = ?
1390
+ .prepare(`UPDATE tasks SET status = ?, assigned_binding_id = ?, result_summary = ?, updated_at = ?
1391
1391
  WHERE task_id = ?`)
1392
1392
  .run(next, assigned, summary, now, task.task_id);
1393
1393
  return { ...task, status: next, assigned_binding_id: assigned, result_summary: summary, updated_at: now };
1394
1394
  }
1395
+ /** Targeted ACCEPT compare-and-set; a stale Task cannot overwrite a newer relation or state. */
1396
+ transitionTaskIfCurrent(task, to) {
1397
+ const next = transition(asTaskStatus(task.status), to);
1398
+ const now = new Date().toISOString();
1399
+ const result = this.db.prepare(`UPDATE tasks SET status = ?, updated_at = ?
1400
+ WHERE task_id = ? AND status = ? AND territory_id = ?
1401
+ AND assigned_binding_id IS ? AND result_summary IS ? AND updated_at = ?`).run(next, now, task.task_id, task.status, task.territory_id, task.assigned_binding_id, task.result_summary, task.updated_at);
1402
+ if (result.changes !== 1)
1403
+ throw new Error('TASK_STATE_CHANGED: 当前任务关系或状态已变化,拒绝过期状态更新。');
1404
+ return { ...task, status: next, updated_at: now };
1405
+ }
1395
1406
  // ── worker_results(Phase 2 第 6 张表;保存 Claim,不是 Fact)──
1396
1407
  listWorkerResults(taskId) {
1397
1408
  return this.db
@@ -1423,17 +1434,17 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1423
1434
  }
1424
1435
  /** Complete exact budget ledger; display event windows must never be used as accounting history. */
1425
1436
  listBudgetEvents(kingdomId) {
1426
- return this.db.prepare(`SELECT * FROM events WHERE kingdom_id = ? AND event_type IN
1427
- ('BUDGET_POLICY_UPDATED', 'BUDGET_ADMISSION_RESERVED', 'BUDGET_ADMISSION_BOUND', 'BUDGET_ADMISSION_CANCELLED', 'DISPATCH_INTENDED')
1437
+ return this.db.prepare(`SELECT * FROM events WHERE kingdom_id = ? AND event_type IN
1438
+ ('BUDGET_POLICY_UPDATED', 'BUDGET_ADMISSION_RESERVED', 'BUDGET_ADMISSION_BOUND', 'BUDGET_ADMISSION_CANCELLED', 'DISPATCH_INTENDED')
1428
1439
  ORDER BY seq ASC`).all(kingdomId);
1429
1440
  }
1430
1441
  listWorkspaceAdmissionEvents(kingdomId) {
1431
- return this.db.prepare(`SELECT * FROM events WHERE kingdom_id = ? AND event_type IN
1432
- ('WORKSPACE_ADMISSION_RESERVED', 'WORKSPACE_ADMISSION_BOUND', 'WORKSPACE_ADMISSION_CANCELLED')
1442
+ return this.db.prepare(`SELECT * FROM events WHERE kingdom_id = ? AND event_type IN
1443
+ ('WORKSPACE_ADMISSION_RESERVED', 'WORKSPACE_ADMISSION_BOUND', 'WORKSPACE_ADMISSION_CANCELLED')
1433
1444
  ORDER BY seq ASC`).all(kingdomId);
1434
1445
  }
1435
1446
  listRuntimeCostEvents(kingdomId) {
1436
- return this.db.prepare(`SELECT * FROM events WHERE kingdom_id = ? AND event_type IN
1447
+ return this.db.prepare(`SELECT * FROM events WHERE kingdom_id = ? AND event_type IN
1437
1448
  ('RUNTIME_ROLE_USAGE_OBSERVED', 'RUNTIME_PROMPT_COST_OBSERVED') ORDER BY seq ASC`).all(kingdomId);
1438
1449
  }
1439
1450
  listActorEventsSince(kingdomId, actorId, afterSeq) {
@@ -1471,8 +1482,8 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1471
1482
  }
1472
1483
  insertWorkerResult(row) {
1473
1484
  this.db
1474
- .prepare(`INSERT INTO worker_results
1475
- (result_id, task_id, attempt_no, worker_binding_id, session_id, outcome, result_json, created_at)
1485
+ .prepare(`INSERT INTO worker_results
1486
+ (result_id, task_id, attempt_no, worker_binding_id, session_id, outcome, result_json, created_at)
1476
1487
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1477
1488
  .run(row.result_id, row.task_id, row.attempt_no, row.worker_binding_id, row.session_id, row.outcome, row.result_json, row.created_at);
1478
1489
  return row;
@@ -1498,21 +1509,21 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1498
1509
  /** 王国内所有未终结的 Execution(人物应当在场的那些)。 */
1499
1510
  listLiveExecutions(kingdomId) {
1500
1511
  return this.db
1501
- .prepare(`SELECT e.* FROM executions e
1502
- JOIN tasks t ON t.task_id = e.task_id
1503
- JOIN territories te ON te.territory_id = t.territory_id
1504
- WHERE te.kingdom_id = ?
1505
- AND e.state IN ('STARTING', 'RUNNING', 'PAUSED')
1512
+ .prepare(`SELECT e.* FROM executions e
1513
+ JOIN tasks t ON t.task_id = e.task_id
1514
+ JOIN territories te ON te.territory_id = t.territory_id
1515
+ WHERE te.kingdom_id = ?
1516
+ AND e.state IN ('STARTING', 'RUNNING', 'PAUSED')
1506
1517
  ORDER BY e.started_at`)
1507
1518
  .all(kingdomId);
1508
1519
  }
1509
1520
  insertExecution(row) {
1510
1521
  this.db
1511
- .prepare(`INSERT INTO executions
1512
- (execution_id, task_id, attempt_no, worker_binding_id, session_id, state, detail,
1513
- started_at, heartbeat_at, ended_at, pause_requested_at,
1514
- executor_kind, provider, provider_source, requested_model, resolved_model, model_source,
1515
- execution_profile_json, execution_contract, lease_id, capability_decision_id)
1522
+ .prepare(`INSERT INTO executions
1523
+ (execution_id, task_id, attempt_no, worker_binding_id, session_id, state, detail,
1524
+ started_at, heartbeat_at, ended_at, pause_requested_at,
1525
+ executor_kind, provider, provider_source, requested_model, resolved_model, model_source,
1526
+ execution_profile_json, execution_contract, lease_id, capability_decision_id)
1516
1527
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1517
1528
  .run(row.execution_id, row.task_id, row.attempt_no, row.worker_binding_id, row.session_id, row.state, row.detail, row.started_at, row.heartbeat_at, row.ended_at, row.pause_requested_at, row.executor_kind, row.provider, row.provider_source, row.requested_model, row.resolved_model, row.model_source, row.execution_profile_json, row.execution_contract ?? 'LEGACY_COMPAT', row.lease_id ?? null, row.capability_decision_id ?? null);
1518
1529
  return row;
@@ -1545,8 +1556,8 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1545
1556
  ? execution.pause_requested_at
1546
1557
  : patch.pauseRequestedAt;
1547
1558
  this.db
1548
- .prepare(`UPDATE executions
1549
- SET state = ?, detail = ?, session_id = ?, heartbeat_at = ?, ended_at = ?, pause_requested_at = ?
1559
+ .prepare(`UPDATE executions
1560
+ SET state = ?, detail = ?, session_id = ?, heartbeat_at = ?, ended_at = ?, pause_requested_at = ?
1550
1561
  WHERE execution_id = ?`)
1551
1562
  .run(next, detail, sessionId, now, ended, pauseRequestedAt, execution.execution_id);
1552
1563
  return {
@@ -1569,9 +1580,9 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1569
1580
  // session_territory_affinities ────────────────────────────────
1570
1581
  insertAffinity(row) {
1571
1582
  this.db
1572
- .prepare(`INSERT INTO session_territory_affinities
1573
- (affinity_id, kingdom_id, worker_binding_id, runtime_type, runtime_instance_ref,
1574
- session_ref, territory_id, established_at, retired_at, is_current, created_at)
1583
+ .prepare(`INSERT INTO session_territory_affinities
1584
+ (affinity_id, kingdom_id, worker_binding_id, runtime_type, runtime_instance_ref,
1585
+ session_ref, territory_id, established_at, retired_at, is_current, created_at)
1575
1586
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1576
1587
  .run(row.affinity_id, row.kingdom_id, row.worker_binding_id, row.runtime_type, row.runtime_instance_ref, row.session_ref, row.territory_id, row.established_at, row.retired_at ?? null, row.is_current ?? 1, row.created_at);
1577
1588
  return row;
@@ -1582,7 +1593,7 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1582
1593
  }
1583
1594
  getAffinityBySession(session) {
1584
1595
  const rows = this.db
1585
- .prepare(`SELECT * FROM session_territory_affinities
1596
+ .prepare(`SELECT * FROM session_territory_affinities
1586
1597
  WHERE runtime_type = ? AND runtime_instance_ref = ? AND session_ref = ?`)
1587
1598
  .all(session.runtimeType, session.runtimeInstanceRef, session.sessionRef);
1588
1599
  return rows[0] ?? null;
@@ -1595,7 +1606,7 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1595
1606
  /** Worker 当前(未退役)Affinity;DB 部分唯一索引保证至多一条。 */
1596
1607
  getCurrentAffinityForWorker(kingdomId, workerBindingId) {
1597
1608
  const rows = this.db
1598
- .prepare(`SELECT * FROM session_territory_affinities
1609
+ .prepare(`SELECT * FROM session_territory_affinities
1599
1610
  WHERE kingdom_id = ? AND worker_binding_id = ? AND is_current = 1 LIMIT 1`)
1600
1611
  .all(kingdomId, workerBindingId);
1601
1612
  return rows[0] ?? null;
@@ -1613,11 +1624,11 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1613
1624
  // execution_leases ────────────────────────────────────────────
1614
1625
  insertLease(row) {
1615
1626
  this.db
1616
- .prepare(`INSERT INTO execution_leases
1617
- (lease_id, kingdom_id, worker_binding_id, runtime_type, runtime_instance_ref,
1618
- session_ref, territory_id, task_id, attempt_no, state,
1619
- capability_decision_id, enforcement_plan_snapshot, release_evidence_json, release_reason,
1620
- acquired_at, released_at, updated_at)
1627
+ .prepare(`INSERT INTO execution_leases
1628
+ (lease_id, kingdom_id, worker_binding_id, runtime_type, runtime_instance_ref,
1629
+ session_ref, territory_id, task_id, attempt_no, state,
1630
+ capability_decision_id, enforcement_plan_snapshot, release_evidence_json, release_reason,
1631
+ acquired_at, released_at, updated_at)
1621
1632
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1622
1633
  .run(row.lease_id, row.kingdom_id, row.worker_binding_id, row.runtime_type, row.runtime_instance_ref, row.session_ref, row.territory_id, row.task_id, row.attempt_no, row.state, row.capability_decision_id ?? null, row.enforcement_plan_snapshot ?? null, row.release_evidence_json ?? null, row.release_reason ?? null, row.acquired_at, row.released_at ?? null, row.updated_at);
1623
1634
  return row;
@@ -1640,8 +1651,8 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1640
1651
  /** 某 Session 当前未释放的 Lease(部分唯一索引保证至多一条非 RELEASED)。 */
1641
1652
  getActiveLeaseForSession(session) {
1642
1653
  const rows = this.db
1643
- .prepare(`SELECT * FROM execution_leases
1644
- WHERE runtime_type = ? AND runtime_instance_ref = ? AND session_ref = ? AND state <> 'RELEASED'
1654
+ .prepare(`SELECT * FROM execution_leases
1655
+ WHERE runtime_type = ? AND runtime_instance_ref = ? AND session_ref = ? AND state <> 'RELEASED'
1645
1656
  LIMIT 1`)
1646
1657
  .all(session.runtimeType, session.runtimeInstanceRef, session.sessionRef);
1647
1658
  return rows[0] ?? null;
@@ -1689,11 +1700,11 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1689
1700
  // capability_decisions ────────────────────────────────────────
1690
1701
  insertCapabilityDecision(row) {
1691
1702
  this.db
1692
- .prepare(`INSERT INTO capability_decisions
1693
- (decision_id, kingdom_id, task_id, worker_binding_id, supervisor_binding_id,
1694
- requirement_snapshot, ceiling_snapshot, proposed_grant_snapshot, scope_snapshot, effective_snapshot,
1695
- decision, enforcement_status, enforcement_evidence_json, requirement_coverage,
1696
- reason_code, execution_id, created_at)
1703
+ .prepare(`INSERT INTO capability_decisions
1704
+ (decision_id, kingdom_id, task_id, worker_binding_id, supervisor_binding_id,
1705
+ requirement_snapshot, ceiling_snapshot, proposed_grant_snapshot, scope_snapshot, effective_snapshot,
1706
+ decision, enforcement_status, enforcement_evidence_json, requirement_coverage,
1707
+ reason_code, execution_id, created_at)
1697
1708
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1698
1709
  .run(row.decision_id, row.kingdom_id, row.task_id, row.worker_binding_id ?? null, row.supervisor_binding_id ?? null, row.requirement_snapshot ?? null, row.ceiling_snapshot ?? null, row.proposed_grant_snapshot ?? null, row.scope_snapshot ?? null, row.effective_snapshot ?? null, row.decision, row.enforcement_status, row.enforcement_evidence_json ?? null, row.requirement_coverage ?? 'NONE', row.reason_code ?? null, row.execution_id ?? null, row.created_at);
1699
1710
  return row;
@@ -1715,12 +1726,12 @@ BEGIN SELECT RAISE(ABORT, 'DISPATCH_NO_DELETE'); END;
1715
1726
  // dispatch_records ────────────────────────────────────────────
1716
1727
  insertDispatchIntent(row) {
1717
1728
  this.db
1718
- .prepare(`INSERT INTO dispatch_records
1719
- (dispatch_id, kingdom_id, lease_id, execution_id, task_id, attempt_no,
1720
- runtime_type, runtime_instance_ref, session_ref, state,
1721
- dispatch_request_snapshot, dispatch_input_ref_json, dispatch_payload_hash,
1722
- runtime_dispatch_ref, runtime_execution_ref, receipt_json, terminal_evidence_json,
1723
- output_ref_json, dispatched_at, receipt_at, terminal_at, created_at, updated_at)
1729
+ .prepare(`INSERT INTO dispatch_records
1730
+ (dispatch_id, kingdom_id, lease_id, execution_id, task_id, attempt_no,
1731
+ runtime_type, runtime_instance_ref, session_ref, state,
1732
+ dispatch_request_snapshot, dispatch_input_ref_json, dispatch_payload_hash,
1733
+ runtime_dispatch_ref, runtime_execution_ref, receipt_json, terminal_evidence_json,
1734
+ output_ref_json, dispatched_at, receipt_at, terminal_at, created_at, updated_at)
1724
1735
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1725
1736
  .run(row.dispatch_id, row.kingdom_id, row.lease_id, row.execution_id, row.task_id, row.attempt_no, row.runtime_type, row.runtime_instance_ref, row.session_ref, row.state, row.dispatch_request_snapshot, row.dispatch_input_ref_json, row.dispatch_payload_hash, row.runtime_dispatch_ref ?? null, row.runtime_execution_ref ?? null, row.receipt_json ?? null, row.terminal_evidence_json ?? null, row.output_ref_json ?? null, row.dispatched_at ?? null, row.receipt_at ?? null, row.terminal_at ?? null, row.created_at, row.updated_at);
1726
1737
  return row;