@gaia-ai/addon-remote-drupal 0.10.0 → 0.11.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.
@@ -5,6 +5,10 @@ export declare class DrupalGaiaRemote implements GaiaRemote {
5
5
  constructor(api: JsonApiClient);
6
6
  fetchActiveRuns(id: string): Promise<ActiveRun[]>;
7
7
  activeRunCount(id: string): Promise<number>;
8
+ machineCapacity(hostKey: string): Promise<{
9
+ maxParallel: number;
10
+ currentLoad: number;
11
+ } | null>;
8
12
  claimNext(c: ClaimOptions): Promise<ClaimedRun | null>;
9
13
  getTicket(uuid: string): Promise<Ticket>;
10
14
  getRunWorktree(uuid: string): Promise<string>;
@@ -47,6 +51,13 @@ export declare class DrupalGaiaRemote implements GaiaRemote {
47
51
  * so it degrades to '' and the row still renders.
48
52
  */
49
53
  private projectName;
54
+ /**
55
+ * Host concurrent-run budget for a machine uuid (GAIA-353).
56
+ *
57
+ * Best-effort: an unreadable or missing machine degrades to 0 so the row
58
+ * still lists; capacity is never invented from a conductor field.
59
+ */
60
+ private machineMaxParallel;
50
61
  private projectUuid;
51
62
  }
52
63
  export declare function drupalRemote(): RemotePlugin;
package/dist/src/index.js CHANGED
@@ -34,12 +34,12 @@ export class DrupalGaiaRemote {
34
34
  this.api = api;
35
35
  }
36
36
  async fetchActiveRuns(id) {
37
- // The conductor identity `id` is the machine_id (hash of hostname+path).
37
+ // The conductor identity `id` is the registration key (conductor_id).
38
38
  // gaia_run links to its owning conductor via the `conductor_id` entity-ref;
39
- // filter on the related conductor's `machine_id` (nested relationship filter).
39
+ // filter on the related conductor's registration string field.
40
40
  const rows = await this.api
41
41
  .collection('gaia_run')
42
- .where('conductor_id.machine_id', '=', id)
42
+ .where('conductor_id.conductor_id', '=', id)
43
43
  .whereIn('state', ACTIVE)
44
44
  .fields(['state', 'state_at_start', 'worktree_path'])
45
45
  .page(100)
@@ -55,22 +55,39 @@ export class DrupalGaiaRemote {
55
55
  })));
56
56
  }
57
57
  async activeRunCount(id) {
58
- // Filter on the related conductor's `machine_id` (nested relationship filter).
58
+ // Filter on the related conductor's registration string field.
59
59
  return this.api
60
60
  .collection('gaia_run')
61
- .where('conductor_id.machine_id', '=', id)
61
+ .where('conductor_id.conductor_id', '=', id)
62
62
  .whereIn('state', ACTIVE)
63
63
  .fields(['drupal_internal__id'])
64
64
  .page(100)
65
65
  .count();
66
66
  }
67
+ async machineCapacity(hostKey) {
68
+ // Shared host budget (GAIA-353 D5): read gaia_machine after heartbeat so
69
+ // current_load is peers + this process. Filter on host_key (MachineContext
70
+ // stem), not the composed conductor_id.
71
+ const row = await this.api
72
+ .collection('gaia_machine')
73
+ .where('host_key', '=', hostKey)
74
+ .fields(['max_parallel', 'current_load'])
75
+ .first();
76
+ if (!row)
77
+ return null;
78
+ return {
79
+ maxParallel: row.attr('max_parallel') ?? 1,
80
+ currentLoad: row.attr('current_load') ?? 0,
81
+ };
82
+ }
67
83
  async claimNext(c) {
68
84
  const res = (await this.api.post('gaia/claim-next', {
69
85
  data: {
70
86
  attributes: {
71
87
  lease_seconds: c.leaseSeconds,
72
88
  // Server (`ConductorResolverTrait`) narrows to the caller's conductor
73
- // by the `machine_id` attribute; `c.conductorId` IS the machine_id.
89
+ // by conductor_id (legacy machine_id still accepted).
90
+ conductor_id: c.conductorId,
74
91
  machine_id: c.conductorId,
75
92
  },
76
93
  },
@@ -160,14 +177,16 @@ export class DrupalGaiaRemote {
160
177
  return ticket.attr('branch_name') ?? '';
161
178
  }
162
179
  async registerConductor(reg) {
163
- const r = await this.api.upsert('gaia_conductor', { path: 'machine_id', value: reg.id }, {
180
+ // Heartbeat is the authoritative upsert (creates machine + conductor).
181
+ // Keep JSON:API upsert as a best-effort create path for attributes the
182
+ // heartbeat already covers; key on conductor_id (GAIA-353).
183
+ const r = await this.api.upsert('gaia_conductor', { path: 'conductor_id', value: reg.id }, {
164
184
  attributes: {
165
- machine_id: reg.id,
185
+ conductor_id: reg.id,
166
186
  status: 'online',
167
187
  workspace_root: reg.workspace,
168
188
  label: reg.label,
169
189
  states: reg.states,
170
- max_parallel: reg.max_parallel,
171
190
  ...processIdentity(reg),
172
191
  },
173
192
  relationships: {
@@ -185,13 +204,14 @@ export class DrupalGaiaRemote {
185
204
  return r.id; // drupal uuid
186
205
  }
187
206
  async heartbeat(reg, load, lease = 300) {
188
- // Server-side upsert by machine_id (see HeartbeatResource): never 404s,
207
+ // Server-side upsert by conductor_id (see HeartbeatResource): never 404s,
189
208
  // heals a vanished registration, and returns the resulting status. Sending
190
209
  // the full registration lets the server recreate the entity when missing.
191
210
  const res = (await this.api.post('gaia/heartbeat', {
192
211
  data: {
193
212
  attributes: {
194
- machine_id: reg.id,
213
+ conductor_id: reg.id,
214
+ host_key: reg.host_key ?? reg.id,
195
215
  project: reg.project,
196
216
  states: reg.states,
197
217
  workspace_root: reg.workspace,
@@ -209,7 +229,7 @@ export class DrupalGaiaRemote {
209
229
  async getConductorStatus(id) {
210
230
  const c = await this.api
211
231
  .collection('gaia_conductor')
212
- .where('machine_id', '=', id)
232
+ .where('conductor_id', '=', id)
213
233
  .fields(['status'])
214
234
  .first();
215
235
  return c ? (c.attr('status') ?? null) : null;
@@ -217,7 +237,7 @@ export class DrupalGaiaRemote {
217
237
  async setConductorStatus(id, status) {
218
238
  const c = await this.api
219
239
  .collection('gaia_conductor')
220
- .where('machine_id', '=', id)
240
+ .where('conductor_id', '=', id)
221
241
  .first();
222
242
  if (c) {
223
243
  await this.api.update('gaia_conductor', c.id, { attributes: { status } });
@@ -232,10 +252,16 @@ export class DrupalGaiaRemote {
232
252
  // Resolve each distinct project once rather than per row: a machine
233
253
  // typically runs several conductors of the same project.
234
254
  const projectNames = new Map();
255
+ // Host capacity SoT is gaia_machine (GAIA-353); conductor.max_parallel is gone.
256
+ const machineMaxParallel = new Map();
235
257
  for (const r of rows) {
236
- const uuid = r.rel('project_id');
237
- if (uuid && !projectNames.has(uuid)) {
238
- projectNames.set(uuid, await this.projectName(uuid));
258
+ const projectUuid = r.rel('project_id');
259
+ if (projectUuid && !projectNames.has(projectUuid)) {
260
+ projectNames.set(projectUuid, await this.projectName(projectUuid));
261
+ }
262
+ const machineUuid = r.rel('machine');
263
+ if (machineUuid && !machineMaxParallel.has(machineUuid)) {
264
+ machineMaxParallel.set(machineUuid, await this.machineMaxParallel(machineUuid));
239
265
  }
240
266
  }
241
267
  return rows.map((r) => {
@@ -245,16 +271,21 @@ export class DrupalGaiaRemote {
245
271
  const pid = r.attr('last_pid');
246
272
  const name = r.attr('conductor_name');
247
273
  return {
248
- id: r.attr('machine_id') ?? r.id,
274
+ id: r.attr('conductor_id') ??
275
+ r.attr('machine_id') ??
276
+ r.id,
249
277
  project: projectNames.get(r.rel('project_id') ?? '') ?? '',
250
278
  label: r.attr('label') ?? '',
251
279
  status: r.attr('status') ?? '',
252
280
  // ISO-8601 over the wire, not the unix integer the field name implies.
253
281
  lastSeen: toUnixSeconds(r.attr('last_seen')) ?? 0,
254
- load: r.attr('current_load') ?? 0,
282
+ // Process load telemetry; host capacity is on gaia_machine (GAIA-353).
283
+ load: r.attr('reported_load') ??
284
+ r.attr('current_load') ??
285
+ 0,
255
286
  workspaceRoot: r.attr('workspace_root') ?? '',
256
287
  leaseExpiresAt: toUnixSeconds(r.attr('lease_expires_at')) ?? 0,
257
- maxParallel: r.attr('max_parallel') ?? 0,
288
+ maxParallel: machineMaxParallel.get(r.rel('machine') ?? '') ?? 0,
258
289
  ...(typeof pid === 'number' ? { lastPid: pid } : {}),
259
290
  ...(typeof name === 'string' && name !== ''
260
291
  ? { conductorName: name }
@@ -287,7 +318,7 @@ export class DrupalGaiaRemote {
287
318
  async fetchFinalizableRuns(id) {
288
319
  const rows = await this.api
289
320
  .collection('gaia_run')
290
- .where('conductor_id.machine_id', '=', id)
321
+ .where('conductor_id.conductor_id', '=', id)
291
322
  .where('state', '=', 'done')
292
323
  .where('closed', '=', '0')
293
324
  .fields(['worktree_path', 'agent', 'drupal_internal__id'])
@@ -332,7 +363,7 @@ export class DrupalGaiaRemote {
332
363
  const fields = ['branch_name', 'state', 'closed'];
333
364
  const done = await this.api
334
365
  .collection('gaia_ticket')
335
- .where('conductor_id.machine_id', '=', id)
366
+ .where('conductor_id.conductor_id', '=', id)
336
367
  .whereIn('state', TERMINAL)
337
368
  .where('cleaned_up', '=', '0')
338
369
  .fields(fields)
@@ -340,7 +371,7 @@ export class DrupalGaiaRemote {
340
371
  .list();
341
372
  const closed = await this.api
342
373
  .collection('gaia_ticket')
343
- .where('conductor_id.machine_id', '=', id)
374
+ .where('conductor_id.conductor_id', '=', id)
344
375
  .where('closed', '=', '1')
345
376
  .where('cleaned_up', '=', '0')
346
377
  .fields(fields)
@@ -462,6 +493,21 @@ export class DrupalGaiaRemote {
462
493
  return '';
463
494
  }
464
495
  }
496
+ /**
497
+ * Host concurrent-run budget for a machine uuid (GAIA-353).
498
+ *
499
+ * Best-effort: an unreadable or missing machine degrades to 0 so the row
500
+ * still lists; capacity is never invented from a conductor field.
501
+ */
502
+ async machineMaxParallel(uuid) {
503
+ try {
504
+ const m = await this.api.resource('gaia_machine', uuid);
505
+ return m.attr('max_parallel') ?? 0;
506
+ }
507
+ catch {
508
+ return 0;
509
+ }
510
+ }
465
511
  async projectUuid(name) {
466
512
  // A project's name→uuid never changes, so cache it: gatherBatch resolves
467
513
  // many identifiers per `gaia deployment tickets` call, each of which would
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/addon-remote-drupal",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "GAIA conductor remote addon: the Drupal/JSON:API control-plane remote.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,10 +20,10 @@
20
20
  "directory": "gaia-cli/addons/remote-drupal"
21
21
  },
22
22
  "dependencies": {
23
- "dropsh": "^0.5.8"
23
+ "dropsh": "^0.6.1"
24
24
  },
25
25
  "peerDependencies": {
26
- "@gaia-ai/conductor": "^0.10.0",
27
- "@gaia-ai/core": "^0.10.0"
26
+ "@gaia-ai/conductor": "^0.11.0",
27
+ "@gaia-ai/core": "^0.11.0"
28
28
  }
29
29
  }