@enderfga/claw-orchestrator 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +28 -28
  2. package/dist/bin/cli.js +107 -1
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/src/acp-server.d.ts +5 -5
  5. package/dist/src/acp-server.js +3 -3
  6. package/dist/src/acp-server.js.map +1 -1
  7. package/dist/src/autoloop/dispatcher.d.ts +22 -0
  8. package/dist/src/autoloop/dispatcher.js +71 -13
  9. package/dist/src/autoloop/dispatcher.js.map +1 -1
  10. package/dist/src/autoloop/messages.d.ts +10 -0
  11. package/dist/src/autoloop/messages.js.map +1 -1
  12. package/dist/src/autoloop/runner.js +6 -0
  13. package/dist/src/autoloop/runner.js.map +1 -1
  14. package/dist/src/constants.d.ts +0 -6
  15. package/dist/src/constants.js +0 -6
  16. package/dist/src/constants.js.map +1 -1
  17. package/dist/src/council.d.ts +15 -0
  18. package/dist/src/council.js +48 -35
  19. package/dist/src/council.js.map +1 -1
  20. package/dist/src/dashboard/index.html +191 -6
  21. package/dist/src/embedded-server.js +132 -9
  22. package/dist/src/embedded-server.js.map +1 -1
  23. package/dist/src/fanout.d.ts +30 -1
  24. package/dist/src/fanout.js +32 -3
  25. package/dist/src/fanout.js.map +1 -1
  26. package/dist/src/index.d.ts +1 -0
  27. package/dist/src/index.js +360 -4
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/kernel/agent-step.d.ts +59 -0
  30. package/dist/src/kernel/agent-step.js +100 -0
  31. package/dist/src/kernel/agent-step.js.map +1 -0
  32. package/dist/src/kernel/conditions.d.ts +11 -0
  33. package/dist/src/kernel/conditions.js +24 -0
  34. package/dist/src/kernel/conditions.js.map +1 -0
  35. package/dist/src/kernel/engine.d.ts +319 -0
  36. package/dist/src/kernel/engine.js +1047 -0
  37. package/dist/src/kernel/engine.js.map +1 -0
  38. package/dist/src/kernel/exec.d.ts +43 -0
  39. package/dist/src/kernel/exec.js +112 -0
  40. package/dist/src/kernel/exec.js.map +1 -0
  41. package/dist/src/kernel/file-lock.d.ts +50 -0
  42. package/dist/src/kernel/file-lock.js +135 -0
  43. package/dist/src/kernel/file-lock.js.map +1 -0
  44. package/dist/src/kernel/nodes/agent.d.ts +4 -0
  45. package/dist/src/kernel/nodes/agent.js +35 -0
  46. package/dist/src/kernel/nodes/agent.js.map +1 -0
  47. package/dist/src/kernel/nodes/autoloop.d.ts +78 -0
  48. package/dist/src/kernel/nodes/autoloop.js +75 -0
  49. package/dist/src/kernel/nodes/autoloop.js.map +1 -0
  50. package/dist/src/kernel/nodes/council.d.ts +12 -0
  51. package/dist/src/kernel/nodes/council.js +88 -0
  52. package/dist/src/kernel/nodes/council.js.map +1 -0
  53. package/dist/src/kernel/nodes/fanout.d.ts +11 -0
  54. package/dist/src/kernel/nodes/fanout.js +63 -0
  55. package/dist/src/kernel/nodes/fanout.js.map +1 -0
  56. package/dist/src/kernel/nodes/human-gate.d.ts +4 -0
  57. package/dist/src/kernel/nodes/human-gate.js +7 -0
  58. package/dist/src/kernel/nodes/human-gate.js.map +1 -0
  59. package/dist/src/kernel/nodes/index.d.ts +12 -0
  60. package/dist/src/kernel/nodes/index.js +21 -0
  61. package/dist/src/kernel/nodes/index.js.map +1 -0
  62. package/dist/src/kernel/nodes/router.d.ts +4 -0
  63. package/dist/src/kernel/nodes/router.js +12 -0
  64. package/dist/src/kernel/nodes/router.js.map +1 -0
  65. package/dist/src/kernel/nodes/subflow.d.ts +13 -0
  66. package/dist/src/kernel/nodes/subflow.js +38 -0
  67. package/dist/src/kernel/nodes/subflow.js.map +1 -0
  68. package/dist/src/kernel/nodes/ultraapp.d.ts +60 -0
  69. package/dist/src/kernel/nodes/ultraapp.js +62 -0
  70. package/dist/src/kernel/nodes/ultraapp.js.map +1 -0
  71. package/dist/src/kernel/nodes/verifier.d.ts +14 -0
  72. package/dist/src/kernel/nodes/verifier.js +84 -0
  73. package/dist/src/kernel/nodes/verifier.js.map +1 -0
  74. package/dist/src/kernel/projections.d.ts +42 -0
  75. package/dist/src/kernel/projections.js +133 -0
  76. package/dist/src/kernel/projections.js.map +1 -0
  77. package/dist/src/kernel/repo.d.ts +13 -0
  78. package/dist/src/kernel/repo.js +64 -0
  79. package/dist/src/kernel/repo.js.map +1 -0
  80. package/dist/src/kernel/secrets.d.ts +25 -0
  81. package/dist/src/kernel/secrets.js +48 -0
  82. package/dist/src/kernel/secrets.js.map +1 -0
  83. package/dist/src/kernel/store.d.ts +225 -0
  84. package/dist/src/kernel/store.js +838 -0
  85. package/dist/src/kernel/store.js.map +1 -0
  86. package/dist/src/kernel/templates/index.d.ts +140 -0
  87. package/dist/src/kernel/templates/index.js +266 -0
  88. package/dist/src/kernel/templates/index.js.map +1 -0
  89. package/dist/src/kernel/types.d.ts +326 -0
  90. package/dist/src/kernel/types.js +19 -0
  91. package/dist/src/kernel/types.js.map +1 -0
  92. package/dist/src/models.d.ts +1 -1
  93. package/dist/src/models.js +31 -3
  94. package/dist/src/models.js.map +1 -1
  95. package/dist/src/persistent-cursor-session.js +6 -1
  96. package/dist/src/persistent-cursor-session.js.map +1 -1
  97. package/dist/src/persistent-grok-session.d.ts +40 -0
  98. package/dist/src/persistent-grok-session.js +197 -0
  99. package/dist/src/persistent-grok-session.js.map +1 -0
  100. package/dist/src/run-ledger.d.ts +57 -3
  101. package/dist/src/run-ledger.js +45 -2
  102. package/dist/src/run-ledger.js.map +1 -1
  103. package/dist/src/session-manager.d.ts +176 -129
  104. package/dist/src/session-manager.js +657 -603
  105. package/dist/src/session-manager.js.map +1 -1
  106. package/dist/src/types.d.ts +37 -4
  107. package/dist/src/types.js +15 -1
  108. package/dist/src/types.js.map +1 -1
  109. package/dist/src/ultraapp/build.d.ts +117 -3
  110. package/dist/src/ultraapp/build.js +319 -3
  111. package/dist/src/ultraapp/build.js.map +1 -1
  112. package/dist/src/ultraapp/contract.d.ts +52 -0
  113. package/dist/src/ultraapp/contract.js +83 -0
  114. package/dist/src/ultraapp/contract.js.map +1 -0
  115. package/dist/src/ultraapp/conventions.js +9 -2
  116. package/dist/src/ultraapp/conventions.js.map +1 -1
  117. package/dist/src/ultraapp/fix-on-failure.d.ts +21 -2
  118. package/dist/src/ultraapp/fix-on-failure.js +46 -62
  119. package/dist/src/ultraapp/fix-on-failure.js.map +1 -1
  120. package/dist/src/ultraapp/manager.d.ts +107 -2
  121. package/dist/src/ultraapp/manager.js +305 -86
  122. package/dist/src/ultraapp/manager.js.map +1 -1
  123. package/dist/src/verify/baseline.d.ts +73 -0
  124. package/dist/src/verify/baseline.js +186 -0
  125. package/dist/src/verify/baseline.js.map +1 -0
  126. package/dist/src/verify/contract.d.ts +116 -0
  127. package/dist/src/verify/contract.js +142 -0
  128. package/dist/src/verify/contract.js.map +1 -0
  129. package/dist/src/verify/evidence.d.ts +61 -0
  130. package/dist/src/verify/evidence.js +133 -0
  131. package/dist/src/verify/evidence.js.map +1 -0
  132. package/dist/src/verify/runner.d.ts +63 -0
  133. package/dist/src/verify/runner.js +317 -0
  134. package/dist/src/verify/runner.js.map +1 -0
  135. package/openclaw.plugin.json +8 -0
  136. package/package.json +2 -2
  137. package/skills/SKILL.md +121 -80
  138. package/skills/references/acp.md +18 -18
  139. package/skills/references/autoloop.md +148 -72
  140. package/skills/references/claude-cli-tracking.md +4 -4
  141. package/skills/references/cli.md +103 -60
  142. package/skills/references/council.md +109 -37
  143. package/skills/references/dashboard.md +34 -6
  144. package/skills/references/getting-started.md +14 -14
  145. package/skills/references/inbox.md +4 -4
  146. package/skills/references/mcp.md +39 -34
  147. package/skills/references/multi-engine.md +109 -51
  148. package/skills/references/observability.md +88 -27
  149. package/skills/references/openai-compat.md +40 -40
  150. package/skills/references/sessions.md +44 -26
  151. package/skills/references/tools.md +402 -309
  152. package/skills/references/ultra.md +45 -45
  153. package/skills/references/ultraapp.md +126 -50
  154. package/skills/references/verification.md +187 -0
  155. package/skills/references/workflow.md +362 -0
  156. package/dist/src/ultraapp/fix-on-failure-session.d.ts +0 -23
  157. package/dist/src/ultraapp/fix-on-failure-session.js +0 -51
  158. package/dist/src/ultraapp/fix-on-failure-session.js.map +0 -1
@@ -4,15 +4,52 @@
4
4
  * Default concurrency = 1. Pending builds are FIFO. Subscribers receive every
5
5
  * BuildEvent emitted by the worker for any currently-running build.
6
6
  *
7
- * Disk persistence (in-progress + pending lists) is NOT done in v0.2 — if the
8
- * orchestrator restarts mid-build, the build is marked failed and the user
9
- * can rerun. Adding restart resilience is later scope.
7
+ * The queue is durable as of 6.0.0. It used to say so in this comment: "Disk
8
+ * persistence (in-progress + pending lists) is NOT done in v0.2 — if the
9
+ * orchestrator restarts mid-build, the build is marked failed and the user can
10
+ * rerun." In practice a restart did not mark anything: the pending list simply
11
+ * vanished, along with any queued build the user was waiting on, with no record
12
+ * that it had ever been asked for.
13
+ *
14
+ * Now the pending list and the in-flight run id are written on every change and
15
+ * restored on construction. A build that was in flight when the process died is
16
+ * re-queued rather than resumed — each build starts from a fresh council
17
+ * worktree, so re-running is safe and resuming a half-built tree is not.
18
+ *
19
+ * Durability without ownership would be worse than neither: two processes each
20
+ * restoring the same file would each run the same builds, doing every side
21
+ * effect twice. So restoring takes a lease on the state file, held by pid and
22
+ * heartbeated. A queue that cannot take the lease starts empty and leaves the
23
+ * work to whoever owns it.
10
24
  */
11
25
  import type { BuildEvent } from './build-events.js';
12
26
  export type Worker = (runId: string, emit: (e: BuildEvent) => void) => Promise<void>;
13
27
  export interface UltraappBuildQueueOptions {
14
28
  worker: Worker;
15
29
  concurrency?: number;
30
+ /**
31
+ * Where the queue is persisted. Omit for an ephemeral queue — the tests that
32
+ * only exercise ordering do not need a file.
33
+ */
34
+ statePath?: string;
35
+ /** Notified when a restored in-flight build is re-queued. */
36
+ onRestore?: (runIds: string[]) => void;
37
+ /**
38
+ * Notified when another live process owns the queue, so this one starts
39
+ * empty. Not an error — it is the correct outcome.
40
+ */
41
+ onNotOwner?: (owner: QueueOwner) => void;
42
+ }
43
+ export interface QueueOwner {
44
+ /**
45
+ * Identity of the owning queue instance, not of the process.
46
+ *
47
+ * Two queues in one process would otherwise share a pid and each read the
48
+ * other's claim as its own — the same mistake a pid-keyed run lease made.
49
+ */
50
+ ownerId: string;
51
+ pid: number;
52
+ renewedAt: string;
16
53
  }
17
54
  export declare class UltraappBuildQueue {
18
55
  private readonly emitter;
@@ -20,9 +57,78 @@ export declare class UltraappBuildQueue {
20
57
  private currentRunId;
21
58
  private readonly worker;
22
59
  private readonly concurrency;
60
+ private readonly statePath?;
61
+ private heartbeat?;
62
+ /** This queue instance's identity as an owner. Per instance, not per process. */
63
+ readonly ownerId: string;
64
+ /** Consecutive failed dispatch claims, per build. Cleared once it starts. */
65
+ private readonly dispatchRetries;
23
66
  private idlePromise;
24
67
  private idleResolve;
25
68
  constructor(opts: UltraappBuildQueueOptions);
69
+ /** Set when another live queue holds the claim. */
70
+ private notOwner?;
71
+ /**
72
+ * True when this process is executing the queue.
73
+ *
74
+ * Reads the state file rather than answering from the last decision, so a
75
+ * queue that has been taken over reports it even before its next write finds
76
+ * out. Advisory only — nothing acts on this answer; `enqueue` and dispatch
77
+ * re-check under the lock, because a lock-free read is a snapshot and a claim
78
+ * is not.
79
+ */
80
+ ownsQueue(): boolean;
81
+ /** Refuse an operation that only the owner may perform. */
82
+ private assertOwner;
83
+ /** Release the claim so another process can pick the queue up promptly. */
84
+ stop(): void;
85
+ /**
86
+ * Read back a queue left by a previous process.
87
+ *
88
+ * A build that was in flight goes to the FRONT of the restored queue: it was
89
+ * asked for first, and the user has been waiting on it longest.
90
+ */
91
+ /**
92
+ * Claim the queue and read back whatever the last owner left.
93
+ *
94
+ * Read-then-decide is not a claim. Two processes starting with no state file
95
+ * both saw "free", both concluded they owned it, and nothing wrote an owner
96
+ * until the first enqueue — by which time both were running the same builds.
97
+ * The read and the claim happen inside one `O_EXCL` critical section now.
98
+ */
99
+ private restore;
100
+ /**
101
+ * Run `fn` inside the queue's lock, waiting briefly for it.
102
+ *
103
+ * Shared with the run store, and for the same reason: a lock that is merely
104
+ * busy must not be reported the same way as a claim that has moved on.
105
+ */
106
+ private withLock;
107
+ /** The critical section of `restore`: inspect the owner and take it, or step aside. */
108
+ private claimLocked;
109
+ /**
110
+ * Persist the queue, if we still own it.
111
+ *
112
+ * Best-effort about I/O, not about ownership: the check happens inside the
113
+ * lock, in the same critical section as the write. Taking the claim at
114
+ * construction and then writing unconditionally forever was not ownership — a
115
+ * queue whose heartbeat had lapsed and been taken over would quietly stamp
116
+ * itself back in as owner on its next persist, clobbering the new owner's
117
+ * pending list and running its builds a second time.
118
+ */
119
+ private persist;
120
+ /** Caller must hold the lock. Steps aside — permanently — if the claim moved on. */
121
+ private writeStateLocked;
122
+ /**
123
+ * Hand the queue over: stop heartbeating and drop the pending list.
124
+ *
125
+ * Keeping the list would be worse than losing it — every entry now belongs to
126
+ * the new owner, who has its own copy, and dispatching from ours is the double
127
+ * execution the claim exists to prevent. A build already in flight cannot be
128
+ * recalled; it finishes, and nothing further starts.
129
+ */
130
+ private standDown;
131
+ private writeStateUnchecked;
26
132
  /**
27
133
  * Enqueue a build. Resolves immediately after the run is appended to the
28
134
  * pending list. Use {@link idle} to wait for the queue to drain.
@@ -33,6 +139,14 @@ export declare class UltraappBuildQueue {
33
139
  subscribe(listener: (e: BuildEvent) => void): () => void;
34
140
  idle(): Promise<void>;
35
141
  private tryDispatch;
142
+ /**
143
+ * Re-attempt a dispatch that could not claim the state file.
144
+ *
145
+ * Bounded: a queue that cannot record what it is doing must eventually say so
146
+ * rather than spinning, and the build is failed with the reason instead of
147
+ * quietly never starting.
148
+ */
149
+ private _retryDispatch;
36
150
  private emit;
37
151
  private markBusy;
38
152
  private markIdle;
@@ -4,29 +4,293 @@
4
4
  * Default concurrency = 1. Pending builds are FIFO. Subscribers receive every
5
5
  * BuildEvent emitted by the worker for any currently-running build.
6
6
  *
7
- * Disk persistence (in-progress + pending lists) is NOT done in v0.2 — if the
8
- * orchestrator restarts mid-build, the build is marked failed and the user
9
- * can rerun. Adding restart resilience is later scope.
7
+ * The queue is durable as of 6.0.0. It used to say so in this comment: "Disk
8
+ * persistence (in-progress + pending lists) is NOT done in v0.2 — if the
9
+ * orchestrator restarts mid-build, the build is marked failed and the user can
10
+ * rerun." In practice a restart did not mark anything: the pending list simply
11
+ * vanished, along with any queued build the user was waiting on, with no record
12
+ * that it had ever been asked for.
13
+ *
14
+ * Now the pending list and the in-flight run id are written on every change and
15
+ * restored on construction. A build that was in flight when the process died is
16
+ * re-queued rather than resumed — each build starts from a fresh council
17
+ * worktree, so re-running is safe and resuming a half-built tree is not.
18
+ *
19
+ * Durability without ownership would be worse than neither: two processes each
20
+ * restoring the same file would each run the same builds, doing every side
21
+ * effect twice. So restoring takes a lease on the state file, held by pid and
22
+ * heartbeated. A queue that cannot take the lease starts empty and leaves the
23
+ * work to whoever owns it.
10
24
  */
25
+ import crypto from 'node:crypto';
11
26
  import { EventEmitter } from 'node:events';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { withFileLock } from '../kernel/file-lock.js';
30
+ /** How long an owner may go without a heartbeat before the queue is up for grabs. */
31
+ const OWNER_TTL_MS = 60_000;
32
+ /** Refresh interval, comfortably inside the TTL. */
33
+ const HEARTBEAT_MS = 15_000;
34
+ function ownerIsLive(owner, selfId) {
35
+ if (!owner)
36
+ return false;
37
+ if (owner.ownerId === selfId)
38
+ return true;
39
+ const age = Date.now() - Date.parse(owner.renewedAt);
40
+ if (Number.isNaN(age) || age > OWNER_TTL_MS)
41
+ return false;
42
+ try {
43
+ process.kill(owner.pid, 0);
44
+ return true;
45
+ }
46
+ catch (err) {
47
+ return err.code === 'EPERM';
48
+ }
49
+ }
12
50
  export class UltraappBuildQueue {
13
51
  emitter = new EventEmitter();
14
52
  pending = [];
15
53
  currentRunId = null;
16
54
  worker;
17
55
  concurrency;
56
+ statePath;
57
+ heartbeat;
58
+ /** This queue instance's identity as an owner. Per instance, not per process. */
59
+ ownerId = `queue-${crypto.randomUUID()}`;
60
+ /** Consecutive failed dispatch claims, per build. Cleared once it starts. */
61
+ dispatchRetries = new Map();
18
62
  idlePromise = Promise.resolve();
19
63
  idleResolve = null;
20
64
  constructor(opts) {
21
65
  this.worker = opts.worker;
22
66
  this.concurrency = opts.concurrency ?? 1;
67
+ this.statePath = opts.statePath;
68
+ const restored = this.restore();
69
+ if (this.statePath && !this.notOwner) {
70
+ // Independent of any build finishing: a worker that runs for ten minutes
71
+ // makes no state writes, and must not look abandoned for it.
72
+ this.heartbeat = setInterval(() => this.persist(), HEARTBEAT_MS);
73
+ if (typeof this.heartbeat.unref === 'function')
74
+ this.heartbeat.unref();
75
+ }
76
+ if (this.notOwner)
77
+ opts.onNotOwner?.(this.notOwner);
78
+ if (restored.length > 0) {
79
+ opts.onRestore?.(restored);
80
+ this.markBusy();
81
+ void this.tryDispatch();
82
+ }
83
+ }
84
+ /** Set when another live queue holds the claim. */
85
+ notOwner;
86
+ /**
87
+ * True when this process is executing the queue.
88
+ *
89
+ * Reads the state file rather than answering from the last decision, so a
90
+ * queue that has been taken over reports it even before its next write finds
91
+ * out. Advisory only — nothing acts on this answer; `enqueue` and dispatch
92
+ * re-check under the lock, because a lock-free read is a snapshot and a claim
93
+ * is not.
94
+ */
95
+ ownsQueue() {
96
+ if (this.notOwner)
97
+ return false;
98
+ if (!this.statePath)
99
+ return true;
100
+ try {
101
+ const parsed = JSON.parse(fs.readFileSync(this.statePath, 'utf8'));
102
+ if (parsed?.owner && parsed.owner.ownerId !== this.ownerId && ownerIsLive(parsed.owner, this.ownerId)) {
103
+ return false;
104
+ }
105
+ }
106
+ catch {
107
+ // No state file yet, or unreadable: nothing says anyone else has it.
108
+ }
109
+ return true;
110
+ }
111
+ /** Refuse an operation that only the owner may perform. */
112
+ assertOwner(action) {
113
+ if (!this.notOwner)
114
+ return;
115
+ throw new Error(`Cannot ${action}: this build queue is owned by ${this.notOwner.ownerId} (pid ${this.notOwner.pid}) ` +
116
+ `(last seen ${this.notOwner.renewedAt}). Running its builds here would execute them twice.`);
117
+ }
118
+ /** Release the claim so another process can pick the queue up promptly. */
119
+ stop() {
120
+ if (this.heartbeat)
121
+ clearInterval(this.heartbeat);
122
+ this.heartbeat = undefined;
123
+ }
124
+ /**
125
+ * Read back a queue left by a previous process.
126
+ *
127
+ * A build that was in flight goes to the FRONT of the restored queue: it was
128
+ * asked for first, and the user has been waiting on it longest.
129
+ */
130
+ /**
131
+ * Claim the queue and read back whatever the last owner left.
132
+ *
133
+ * Read-then-decide is not a claim. Two processes starting with no state file
134
+ * both saw "free", both concluded they owned it, and nothing wrote an owner
135
+ * until the first enqueue — by which time both were running the same builds.
136
+ * The read and the claim happen inside one `O_EXCL` critical section now.
137
+ */
138
+ restore() {
139
+ if (!this.statePath)
140
+ return [];
141
+ let entered = false;
142
+ const ids = this.withLock(() => {
143
+ entered = true;
144
+ return this.claimLocked();
145
+ }, []);
146
+ if (!entered && !this.notOwner) {
147
+ // We never got inside the critical section — someone else is claiming
148
+ // right now, or the lock file is unusable. Either way we have not taken
149
+ // the queue, and acting as though we had is how both processes end up
150
+ // running the same builds.
151
+ this.notOwner = { ownerId: 'unknown', pid: -1, renewedAt: new Date().toISOString() };
152
+ }
153
+ return ids ?? [];
154
+ }
155
+ /**
156
+ * Run `fn` inside the queue's lock, waiting briefly for it.
157
+ *
158
+ * Shared with the run store, and for the same reason: a lock that is merely
159
+ * busy must not be reported the same way as a claim that has moved on.
160
+ */
161
+ withLock(fn, onContended) {
162
+ if (!this.statePath)
163
+ return undefined;
164
+ const result = withFileLock(`${this.statePath}.lock`, fn, { staleMs: OWNER_TTL_MS, createParent: true });
165
+ return result.ok ? result.value : onContended;
166
+ }
167
+ /** The critical section of `restore`: inspect the owner and take it, or step aside. */
168
+ claimLocked() {
169
+ if (!this.statePath)
170
+ return [];
171
+ let parsed;
172
+ try {
173
+ parsed = JSON.parse(fs.readFileSync(this.statePath, 'utf8'));
174
+ }
175
+ catch {
176
+ parsed = { pending: [], current: null };
177
+ }
178
+ if (ownerIsLive(parsed.owner, this.ownerId)) {
179
+ // Someone else is running these. Taking them would execute every build
180
+ // twice; the honest move is to start empty and say so.
181
+ this.notOwner = parsed.owner;
182
+ return [];
183
+ }
184
+ const ids = [
185
+ ...(parsed.current ? [parsed.current] : []),
186
+ ...(Array.isArray(parsed.pending) ? parsed.pending.filter((x) => typeof x === 'string') : []),
187
+ ];
188
+ for (const runId of ids)
189
+ this.pending.push({ runId });
190
+ // Stake the claim before releasing the lock, so a racer that gets in next
191
+ // sees an owner rather than an empty file.
192
+ this.writeStateUnchecked();
193
+ return ids;
194
+ }
195
+ /**
196
+ * Persist the queue, if we still own it.
197
+ *
198
+ * Best-effort about I/O, not about ownership: the check happens inside the
199
+ * lock, in the same critical section as the write. Taking the claim at
200
+ * construction and then writing unconditionally forever was not ownership — a
201
+ * queue whose heartbeat had lapsed and been taken over would quietly stamp
202
+ * itself back in as owner on its next persist, clobbering the new owner's
203
+ * pending list and running its builds a second time.
204
+ */
205
+ persist() {
206
+ if (!this.statePath)
207
+ return 'ephemeral';
208
+ if (this.notOwner)
209
+ return 'superseded';
210
+ return this.withLock(() => this.writeStateLocked(), 'blocked') ?? 'blocked';
211
+ }
212
+ /** Caller must hold the lock. Steps aside — permanently — if the claim moved on. */
213
+ writeStateLocked() {
214
+ if (!this.statePath)
215
+ return 'ephemeral';
216
+ let parsed;
217
+ try {
218
+ parsed = JSON.parse(fs.readFileSync(this.statePath, 'utf8'));
219
+ }
220
+ catch {
221
+ parsed = undefined;
222
+ }
223
+ if (parsed?.owner && parsed.owner.ownerId !== this.ownerId && ownerIsLive(parsed.owner, this.ownerId)) {
224
+ this.standDown(parsed.owner);
225
+ return 'superseded';
226
+ }
227
+ this.writeStateUnchecked();
228
+ return 'committed';
229
+ }
230
+ /**
231
+ * Hand the queue over: stop heartbeating and drop the pending list.
232
+ *
233
+ * Keeping the list would be worse than losing it — every entry now belongs to
234
+ * the new owner, who has its own copy, and dispatching from ours is the double
235
+ * execution the claim exists to prevent. A build already in flight cannot be
236
+ * recalled; it finishes, and nothing further starts.
237
+ */
238
+ standDown(owner) {
239
+ this.notOwner = owner;
240
+ this.stop();
241
+ this.pending.length = 0;
242
+ if (this.currentRunId === null)
243
+ this.markIdle();
244
+ }
245
+ writeStateUnchecked() {
246
+ if (!this.statePath)
247
+ return;
248
+ try {
249
+ fs.mkdirSync(path.dirname(this.statePath), { recursive: true });
250
+ const state = {
251
+ pending: this.pending.map((p) => p.runId),
252
+ current: this.currentRunId,
253
+ // Every write is also the heartbeat, so a queue that stops making
254
+ // progress stops holding the claim.
255
+ owner: { ownerId: this.ownerId, pid: process.pid, renewedAt: new Date().toISOString() },
256
+ };
257
+ const tmp = `${this.statePath}.tmp.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
258
+ fs.writeFileSync(tmp, JSON.stringify(state));
259
+ fs.renameSync(tmp, this.statePath);
260
+ }
261
+ catch {
262
+ // A queue that cannot be persisted still works for this process.
263
+ }
23
264
  }
24
265
  /**
25
266
  * Enqueue a build. Resolves immediately after the run is appended to the
26
267
  * pending list. Use {@link idle} to wait for the queue to drain.
27
268
  */
28
269
  async enqueue(runId) {
270
+ // Refusing at construction is not enough: `ownsQueue()` returning false
271
+ // while `enqueue` cheerfully ran the build — and rewrote the owner to
272
+ // ourselves — is exactly the double execution the claim exists to stop.
273
+ this.assertOwner('enqueue');
29
274
  this.pending.push({ runId });
275
+ // The claim is re-checked inside the same critical section the write happens
276
+ // in. Owning the queue at construction says nothing about owning it now: the
277
+ // heartbeat can have lapsed and someone else can have taken over, and an
278
+ // `enqueue` that only consulted the construction-time answer would run their
279
+ // builds a second time.
280
+ //
281
+ // Three outcomes, not two. `superseded` refuses. `blocked` — we could not
282
+ // even get into the critical section — also refuses, because an enqueue that
283
+ // cannot be recorded may be an enqueue into a queue somebody else now owns;
284
+ // the earlier version returned a bare `false` here and carried on, and ran a
285
+ // build whose queue state on disk already belonged to another process.
286
+ const persisted = this.persist();
287
+ if (persisted === 'superseded')
288
+ this.assertOwner('enqueue');
289
+ if (persisted === 'blocked') {
290
+ this.pending.pop();
291
+ throw new Error(`Cannot enqueue: the build queue state file is locked by another process, so this build cannot be ` +
292
+ `recorded. Retry in a moment.`);
293
+ }
30
294
  this.markBusy();
31
295
  const pos = this.position(runId);
32
296
  if (pos > 0) {
@@ -38,6 +302,7 @@ export class UltraappBuildQueue {
38
302
  const idx = this.pending.findIndex((p) => p.runId === runId);
39
303
  if (idx >= 0) {
40
304
  this.pending.splice(idx, 1);
305
+ this.persist();
41
306
  this.emit({ type: 'build-cancelled', runId });
42
307
  if (this.pending.length === 0 && this.currentRunId === null)
43
308
  this.markIdle();
@@ -65,6 +330,8 @@ export class UltraappBuildQueue {
65
330
  return this.idlePromise;
66
331
  }
67
332
  async tryDispatch() {
333
+ if (this.notOwner)
334
+ return;
68
335
  if (this.currentRunId !== null)
69
336
  return;
70
337
  if (this.concurrency !== 1)
@@ -75,6 +342,25 @@ export class UltraappBuildQueue {
75
342
  return;
76
343
  }
77
344
  this.currentRunId = next.runId;
345
+ // Same check before starting work as before recording it, and with the same
346
+ // three outcomes: only `committed` (or an ephemeral queue, which owns
347
+ // nothing and persists nothing) may dispatch. Dispatching on `blocked` is
348
+ // how a superseded queue ran a build whose state on disk already named
349
+ // another owner.
350
+ const persisted = this.persist();
351
+ if (persisted === 'superseded') {
352
+ this.currentRunId = null;
353
+ this.markIdle();
354
+ return;
355
+ }
356
+ if (persisted === 'blocked') {
357
+ // Transient: put the build back at the front and try again shortly.
358
+ this.currentRunId = null;
359
+ this.pending.unshift(next);
360
+ this._retryDispatch(next.runId);
361
+ return;
362
+ }
363
+ this.dispatchRetries.delete(next.runId);
78
364
  try {
79
365
  await this.worker(next.runId, (e) => this.emit(e));
80
366
  }
@@ -88,9 +374,39 @@ export class UltraappBuildQueue {
88
374
  }
89
375
  finally {
90
376
  this.currentRunId = null;
377
+ this.persist();
91
378
  void this.tryDispatch();
92
379
  }
93
380
  }
381
+ /**
382
+ * Re-attempt a dispatch that could not claim the state file.
383
+ *
384
+ * Bounded: a queue that cannot record what it is doing must eventually say so
385
+ * rather than spinning, and the build is failed with the reason instead of
386
+ * quietly never starting.
387
+ */
388
+ _retryDispatch(runId) {
389
+ const attempts = (this.dispatchRetries.get(runId) ?? 0) + 1;
390
+ this.dispatchRetries.set(runId, attempts);
391
+ if (attempts > 8) {
392
+ this.dispatchRetries.delete(runId);
393
+ const idx = this.pending.findIndex((p) => p.runId === runId);
394
+ if (idx >= 0)
395
+ this.pending.splice(idx, 1);
396
+ this.emit({
397
+ type: 'build-failed',
398
+ runId,
399
+ phase: 'orchestrator',
400
+ reason: 'the build queue state file stayed locked by another process; the build was not started',
401
+ });
402
+ if (this.pending.length === 0 && this.currentRunId === null)
403
+ this.markIdle();
404
+ return;
405
+ }
406
+ const timer = setTimeout(() => void this.tryDispatch(), 100 * attempts);
407
+ if (typeof timer.unref === 'function')
408
+ timer.unref();
409
+ }
94
410
  emit(e) {
95
411
  this.emitter.emit('event', e);
96
412
  }
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../../../src/ultraapp/build.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../../src/ultraapp/build.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * UltraApp's acceptance contracts — the only mode whose contract is on by default.
3
+ *
4
+ * It earns that because it already ran most of these commands, and because its
5
+ * own documentation has claimed two gates that the code never enforced:
6
+ *
7
+ * 1. `conventions.ts` §4 states "fix-on-failure runs `npm run smoke` and gates
8
+ * build-success on its passing". It did not — the step list was install /
9
+ * build / test / docker-build, with no smoke anywhere. The claim is now true.
10
+ * 2. `ultraapp.md` recorded, as a known limitation since 4.0.0, that the §7g
11
+ * frontend gate "relies on per-agent honesty about running the screenshot
12
+ * capture", with a follow-up promised to make it structural. The capture is
13
+ * now performed by the runtime against the deployed URL and stored as
14
+ * evidence, so whether it happened is no longer an agent's word.
15
+ *
16
+ * What §7g still does not do is judge the pixels. A human or an agent still
17
+ * decides whether the render is acceptable; the runtime guarantees only that the
18
+ * images exist and are of the deployed app. Calling that visual regression would
19
+ * be the same overclaim this module exists to remove.
20
+ */
21
+ import type { AcceptanceContract } from '../verify/contract.js';
22
+ /** §7g's two viewports, unchanged from the convention text agents were given. */
23
+ export declare const UI_VIEWPORTS: {
24
+ width: number;
25
+ height: number;
26
+ label: string;
27
+ }[];
28
+ export interface BuildContractOptions {
29
+ useDocker: boolean;
30
+ fixRounds?: number;
31
+ }
32
+ /**
33
+ * Build-stage checks, run in the council's worktree. The order is the old
34
+ * `DEFAULT_STEPS` order, plus the smoke step the docs already promised.
35
+ */
36
+ export declare function ultraappBuildContract(opts: BuildContractOptions): AcceptanceContract;
37
+ /**
38
+ * Whether a missing/failed screenshot capture should fail the deploy.
39
+ *
40
+ * Default is lenient: capture and store, record a failure, but do not sink a
41
+ * working app because the host has no Chrome. `CLAWO_ULTRAAPP_VISUAL_GATE=strict`
42
+ * makes it binding for setups that want §7g fully enforced.
43
+ */
44
+ export declare function visualGateIsStrict(env?: NodeJS.ProcessEnv): boolean;
45
+ /**
46
+ * Deploy-stage checks, run against the live URL.
47
+ *
48
+ * No health probe here on purpose: `deployArtifact` already polls `/health` and
49
+ * refuses to report success without it, so re-probing would be a second source
50
+ * of truth for a question that is already settled.
51
+ */
52
+ export declare function ultraappDeployContract(url: string, env?: NodeJS.ProcessEnv): AcceptanceContract;