aui-agent-builder 0.3.83 → 0.3.85

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.
@@ -237,208 +237,15 @@ async function _push(pushSpan, agentCode, options = {}) {
237
237
  log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "muted", label: "Dry run \u2014 no changes pushed." }) }));
238
238
  return;
239
239
  }
240
- // ─── Push integration upserts before KB ───
241
- // KB uploads need the integration (and its knowledge base) to exist first.
242
- // Only create/patch/replace integrations here — not deletes: entities/workflows
243
- // may still reference an integration until we push entity changes below.
244
- let integrationUpsertsAlreadyPushed = false;
245
- /** When set, reused in the main Agent Settings push — avoids resolving the draft twice. */
246
- let resolvedPushDraftCache = null;
247
- let cachedPushAgentManagementId;
248
- if (diff && diff.hasChanges) {
249
- const intSession = await getValidSession();
250
- if (intSession) {
251
- const intAsp = await resolveAgentSettingsParams(config, projectConfig, intSession, projectRoot);
252
- if (intAsp) {
253
- const intClient = new AUIClient({
254
- baseUrl: config.apiUrl,
255
- authToken: config.authToken,
256
- accountId: config.accountId,
257
- organizationId: config.organizationId,
258
- environment: config.environment,
259
- });
260
- const intPushLogDir = path.join(projectRoot, ".aui", "push-logs");
261
- fs.mkdirSync(intPushLogDir, { recursive: true });
262
- intClient.setPushLogDir(intPushLogDir);
263
- if (options.apiKey) {
264
- saveAgentSettingsApiKey(options.apiKey);
265
- intClient.setAgentSettingsApiKey(options.apiKey);
266
- }
267
- const intSavedKey = loadAgentSettingsApiKey();
268
- if (intSavedKey && !options.apiKey) {
269
- intClient.setAgentSettingsApiKey(intSavedKey);
270
- }
271
- const intPushTasks = buildPushTasks(diff, fileData, projectRoot, getFileDiff);
272
- const integrationUpsertTasksEarly = intPushTasks.filter(isIntegrationUpsertTask);
273
- if (integrationUpsertTasksEarly.length > 0) {
274
- // Same as main push: integrations must carry agent_version_id on the body.
275
- // Without this, pre-KB upserts omit version_id while parameters / scope_entities
276
- // / rules use agentSettingsParams after resolveVersionDraft (and main skips upserts).
277
- if (projectConfig.version_id || options.versionId) {
278
- resolvedPushDraftCache = await resolveVersionDraft(config, projectConfig, intSession, options.versionId);
279
- intAsp.version_id = resolvedPushDraftCache.versionId;
280
- intAsp.agent_id = resolvedPushDraftCache.agentId;
281
- cachedPushAgentManagementId = resolvedPushDraftCache.agentId;
282
- }
283
- else {
284
- if (!cachedPushAgentManagementId) {
285
- cachedPushAgentManagementId = await resolvePushAgentManagementId(config, projectConfig, intSession, projectRoot);
286
- }
287
- intAsp.agent_id = cachedPushAgentManagementId;
288
- }
289
- log(_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.info, children: [icons.bullet, " Pushing integrations (before KB upload)..."] }) }));
290
- for (const task of integrationUpsertTasksEarly) {
291
- const taskResult = {
292
- label: task.label,
293
- status: "success",
294
- };
295
- try {
296
- await executePushTask(intClient, intAsp, task);
297
- }
298
- catch (err) {
299
- const errMsg = err instanceof Error ? err.message : String(err);
300
- taskResult.status = "failed";
301
- taskResult.error = errMsg;
302
- }
303
- log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
304
- }
305
- integrationUpsertsAlreadyPushed = true;
306
- }
307
- }
308
- }
309
- }
310
- // ─── Knowledge Hubs Push (full files) ───
311
- const { getKnowledgeHubChanges } = await import("../utils/git.js");
312
- const kbChanges = getKnowledgeHubChanges(projectRoot);
313
- if (kbChanges.length > 0) {
314
- const kbConfig = getConfig();
315
- const kbSession = await getValidSession();
316
- const kbNetworkId = projectConfig.agent_id || kbSession?.network_id;
317
- if (kbNetworkId && kbConfig.authToken) {
318
- const { KBViewClient } = await import("../api-client/kb-view-client.js");
319
- const { buildScope, readKbFolder } = await import("../services/kb-view.service.js");
320
- const { loadAgentSettingsApiKey: loadAsKey } = await import("../config/index.js");
321
- const kbViewClient = new KBViewClient({
322
- authToken: kbConfig.authToken,
323
- apiKey: loadAsKey() || undefined,
324
- organizationId: kbConfig.organizationId || "",
325
- environment: kbConfig.environment || "staging",
326
- });
327
- const kbLogDir = path.join(projectRoot, ".aui", "push-logs");
328
- fs.mkdirSync(kbLogDir, { recursive: true });
329
- kbViewClient.setPushLogDir(kbLogDir);
330
- const scope = buildScope({
331
- networkId: kbNetworkId,
332
- organizationId: projectConfig.organization_id || kbConfig.organizationId || "",
333
- accountId: projectConfig.account_id || kbConfig.accountId || "",
334
- });
335
- const userId = kbSession?.user_id || "cli";
336
- // Collect all changed KB directories (skip root-level files)
337
- const changedKBDirs = new Set();
338
- for (const change of kbChanges) {
339
- if (change.kbDirName) {
340
- changedKBDirs.add(change.kbDirName);
341
- }
342
- }
343
- // Split into existing (will upload) and deleted (will delete from server)
344
- const existingKBDirs = [...changedKBDirs].filter((d) => fs.existsSync(path.join(projectRoot, "knowledge-hubs", d)));
345
- const deletedKBDirs = [...changedKBDirs].filter((d) => !fs.existsSync(path.join(projectRoot, "knowledge-hubs", d)));
346
- // Delete KBs that were removed locally
347
- let kbDeleteSucceeded = true;
348
- if (deletedKBDirs.length > 0) {
349
- const { getBaselineFileContent } = await import("../utils/git.js");
350
- const deleteSpinner = startSpinner(`Deleting ${deletedKBDirs.length} knowledge base(s) from server...`);
351
- try {
352
- for (const kbDirName of deletedKBDirs) {
353
- const baselineKb = getBaselineFileContent(projectRoot, `knowledge-hubs/${kbDirName}/kb.json`);
354
- const kbName = baselineKb?.name || kbDirName;
355
- const kbId = baselineKb?.knowledge_base_id;
356
- if (!kbId) {
357
- log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "warning", label: `Cannot delete "${kbName}" — no knowledge_base_id stored. Push the KB first, then delete.` }) }));
358
- continue;
359
- }
360
- await kbViewClient.deleteKnowledgeBase(kbId, scope, kbName);
361
- log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: `Deleted: ${kbName}` }) }));
362
- }
363
- deleteSpinner.succeed(`${deletedKBDirs.length} knowledge base(s) deleted`);
364
- }
365
- catch (error) {
366
- kbDeleteSucceeded = false;
367
- deleteSpinner.fail("Knowledge base deletion failed");
368
- log(_jsx(ErrorDisplay, { error: error }));
369
- }
370
- }
371
- // Upload full files for each changed KB
372
- let kbUploadSucceeded = false;
373
- if (existingKBDirs.length > 0) {
374
- const kbSpinner = startSpinner(`Pushing ${existingKBDirs.length} knowledge base(s)...`);
375
- try {
376
- for (const kbDirName of existingKBDirs) {
377
- const kbDir = path.join(projectRoot, "knowledge-hubs", kbDirName);
378
- const kbData = readKbFolder(kbDir);
379
- if (!kbData)
380
- continue;
381
- const SUPPORTED_EXTENSIONS = new Set([".pdf", ".md", ".txt", ".json"]);
382
- const supportedFiles = kbData.binaryFiles.filter((f) => SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
383
- const skippedFiles = kbData.binaryFiles.filter((f) => !SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
384
- for (const skipped of skippedFiles) {
385
- log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "warning", label: `Skipped unsupported file: ${path.basename(skipped)} (only .pdf, .md, .txt, .json)` }) }));
386
- }
387
- if (supportedFiles.length > 0) {
388
- const importResult = await kbViewClient.importFiles({
389
- files: supportedFiles,
390
- scope,
391
- created_by: userId,
392
- knowledge_base_name: kbData.name,
393
- knowledge_base_description: kbData.description,
394
- });
395
- if (importResult.knowledge_base_id) {
396
- const kbJsonPath = path.join(kbDir, "kb.json");
397
- try {
398
- const raw = JSON.parse(fs.readFileSync(kbJsonPath, "utf-8"));
399
- raw.knowledge_base_id = importResult.knowledge_base_id;
400
- fs.writeFileSync(kbJsonPath, JSON.stringify(raw, null, 2) + "\n");
401
- }
402
- catch { /* kb.json write failed, non-fatal */ }
403
- }
404
- }
405
- }
406
- kbSpinner.succeed(`Knowledge base(s) pushed`);
407
- kbUploadSucceeded = true;
408
- }
409
- catch (error) {
410
- kbSpinner.fail("Knowledge base push failed");
411
- log(_jsx(ErrorDisplay, { error: error }));
412
- }
413
- }
414
- else {
415
- kbUploadSucceeded = true;
416
- }
417
- const kbPushSucceeded = kbUploadSucceeded && kbDeleteSucceeded;
418
- // Commit KB changes to baseline only if push succeeded
419
- if (kbPushSucceeded) {
420
- const kbFilesToAdd = kbChanges
421
- .filter((c) => c.status !== "deleted")
422
- .map((c) => c.file);
423
- const kbFilesToDelete = kbChanges
424
- .filter((c) => c.status === "deleted")
425
- .map((c) => c.file);
426
- if (kbFilesToAdd.length > 0 || kbFilesToDelete.length > 0) {
427
- const { commitBaselineFiles: commitKBFiles, removeBaselineFiles } = await import("../utils/git.js");
428
- if (kbFilesToDelete.length > 0) {
429
- removeBaselineFiles(projectRoot, kbFilesToDelete);
430
- }
431
- if (kbFilesToAdd.length > 0) {
432
- commitKBFiles(projectRoot, kbFilesToAdd, "pushed knowledge hub changes");
433
- }
434
- else {
435
- commitKBFiles(projectRoot, [], "removed knowledge hub files");
436
- }
437
- }
438
- }
439
- }
440
- }
441
240
  // ─── Agent Config Push ───
241
+ //
242
+ // Knowledge Bases used to be pushed here (BEFORE entity writes) with a
243
+ // special pre-step that pushed integrations even earlier so KB uploads
244
+ // would find their integration. That ordering caused two production
245
+ // bugs: integrations were PATCHed before the parameters they reference
246
+ // existed (CTS-12425), and tools were pushed in parallel with their
247
+ // dependencies (CTS-12426). The KB push has been moved into the unified
248
+ // dependency-ordered flow below — see `pushKnowledgeHubs` invocation.
442
249
  if (!diff || !diff.hasChanges) {
443
250
  pushSpan.setAttribute("push.exit_reason", "no_agent_config_changes");
444
251
  log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: "No agent config changes to push." }) }));
@@ -479,20 +286,10 @@ async function _push(pushSpan, agentCode, options = {}) {
479
286
  // available drafts. Push is rejected if no draft is found.
480
287
  let prePushDraft = null;
481
288
  if (projectConfig.version_id || options.versionId) {
482
- prePushDraft =
483
- resolvedPushDraftCache ??
484
- (await resolveVersionDraft(config, projectConfig, session, options.versionId));
289
+ prePushDraft = await resolveVersionDraft(config, projectConfig, session, options.versionId);
485
290
  agentSettingsParams.version_id = prePushDraft.versionId;
486
- agentSettingsParams.agent_id = prePushDraft.agentId;
487
- cachedPushAgentManagementId = prePushDraft.agentId;
488
291
  log(_jsx(StatusLine, { kind: "info", label: `Pushing into draft version: ${prePushDraft.label}` }));
489
292
  }
490
- else {
491
- if (!cachedPushAgentManagementId) {
492
- cachedPushAgentManagementId = await resolvePushAgentManagementId(config, projectConfig, session, projectRoot);
493
- }
494
- agentSettingsParams.agent_id = cachedPushAgentManagementId;
495
- }
496
293
  const pushTasks = buildPushTasks(diff, fileData, projectRoot, getFileDiff);
497
294
  pushSpan.setAttribute("push.task_count", pushTasks.length);
498
295
  if (diff) {
@@ -526,82 +323,55 @@ async function _push(pushSpan, agentCode, options = {}) {
526
323
  };
527
324
  const agentCodeStr = projectConfig.agent_code || projectConfig.agent_id || "unknown";
528
325
  const agentIdStr = projectConfig.agent_id || "unknown";
529
- const pushStep = async (tasks, label, parallel) => {
326
+ /**
327
+ * Run one push step (a group of related tasks for one entity-type)
328
+ * STRICTLY SEQUENTIALLY. There is intentionally no `parallel` flag — the
329
+ * agent-settings backend has no optimistic locking and concurrent writes
330
+ * to the same agent silently merge / drop unresolvable references / re-
331
+ * sequence array items (see file header doc + CTS-12340 / -12425 / -12426
332
+ * for prior incidents). If you think you need to parallelize, you don't.
333
+ */
334
+ const pushStep = async (tasks, label) => {
530
335
  if (tasks.length === 0)
531
336
  return true;
532
337
  log(_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.info, children: [icons.bullet, " ", label, "..."] }) }));
533
338
  const stepFailed = [];
534
339
  try {
535
- if (parallel) {
536
- const results = await Promise.allSettled(tasks.map((t) => executePushTask(client, agentSettingsParams, t)));
537
- for (let i = 0; i < results.length; i++) {
538
- const taskResult = {
539
- label: tasks[i].label,
540
- status: "success",
541
- };
542
- if (results[i].status === "fulfilled") {
543
- succeeded++;
544
- if (tasks[i].file)
545
- succeededFiles.push(tasks[i].file);
546
- }
547
- else {
548
- const err = results[i].reason;
549
- if (isAuthError(err)) {
550
- authFailed = true;
551
- authFailedTasks.push(tasks[i]);
552
- taskResult.status = "auth-failed";
553
- }
554
- else {
555
- failed++;
556
- const errMsg = err instanceof Error ? err.message : String(err);
557
- const failure = {
558
- label: tasks[i].label,
559
- file: tasks[i].file,
560
- error: errMsg,
561
- };
562
- pushFailures.push(failure);
563
- stepFailed.push(failure);
564
- taskResult.status = "failed";
565
- taskResult.error = errMsg;
566
- }
340
+ for (const task of tasks) {
341
+ const taskResult = {
342
+ label: task.label,
343
+ status: "success",
344
+ };
345
+ try {
346
+ const result = await executePushTask(client, agentSettingsParams, task);
347
+ succeeded++;
348
+ if (task.file)
349
+ succeededFiles.push(task.file);
350
+ if (isAlreadyAbsentResult(result)) {
351
+ taskResult.label = `${task.label} (already absent)`;
567
352
  }
568
- log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
569
353
  }
570
- }
571
- else {
572
- for (const task of tasks) {
573
- const taskResult = {
574
- label: task.label,
575
- status: "success",
576
- };
577
- try {
578
- await executePushTask(client, agentSettingsParams, task);
579
- succeeded++;
580
- if (task.file)
581
- succeededFiles.push(task.file);
354
+ catch (err) {
355
+ if (isAuthError(err)) {
356
+ authFailed = true;
357
+ authFailedTasks.push(task);
358
+ taskResult.status = "auth-failed";
582
359
  }
583
- catch (err) {
584
- if (isAuthError(err)) {
585
- authFailed = true;
586
- authFailedTasks.push(task);
587
- taskResult.status = "auth-failed";
588
- }
589
- else {
590
- failed++;
591
- const errMsg = err instanceof Error ? err.message : String(err);
592
- const failure = {
593
- label: task.label,
594
- file: task.file,
595
- error: errMsg,
596
- };
597
- pushFailures.push(failure);
598
- stepFailed.push(failure);
599
- taskResult.status = "failed";
600
- taskResult.error = errMsg;
601
- }
360
+ else {
361
+ failed++;
362
+ const errMsg = err instanceof Error ? err.message : String(err);
363
+ const failure = {
364
+ label: task.label,
365
+ file: task.file,
366
+ error: errMsg,
367
+ };
368
+ pushFailures.push(failure);
369
+ stepFailed.push(failure);
370
+ taskResult.status = "failed";
371
+ taskResult.error = errMsg;
602
372
  }
603
- log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
604
373
  }
374
+ log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
605
375
  }
606
376
  return stepFailed.length === 0 && !authFailed;
607
377
  }
@@ -627,17 +397,45 @@ async function _push(pushSpan, agentCode, options = {}) {
627
397
  t.type === "delete-tool");
628
398
  const settingsTasks = pushTasks.filter((t) => t.type === "patch-general-settings");
629
399
  const rulesTasks = pushTasks.filter((t) => t.type === "put-rules");
630
- await pushStep(paramTasks, "Pushing parameters", false);
631
- // Integrations before entities so workflows/tools can safely reference integration codes.
632
- if (!integrationUpsertsAlreadyPushed) {
633
- await pushStep(integrationUpsertTasks, "Pushing integrations", false);
634
- }
635
- await pushStep(entityTasks, "Pushing entities", false);
636
- // Delete integrations after entity changes drop references where needed.
637
- await pushStep(integrationDeleteTasks, "Removing integrations", false);
638
- await pushStep(toolTasks, "Pushing tools", true);
639
- await pushStep(settingsTasks, "Pushing general settings", false);
640
- await pushStep(rulesTasks, "Pushing rules", false);
400
+ // ─── Push order — see file header for rationale ─────────────────────
401
+ //
402
+ // Phase 1: UPSERTS, top-down by dependency (least → most depends-on).
403
+ // Every step is sequential by construction (`pushStep` has no
404
+ // parallel mode). Do not work around this — the agent-settings
405
+ // backend silently merges / drops unresolvable refs on concurrent
406
+ // writes.
407
+ // 1. Parameters — referenced by entities, integrations, tools, rules.
408
+ await pushStep(paramTasks, "Pushing parameters");
409
+ // 2. Entities reference parameters; referenced by tools, integrations.
410
+ await pushStep(entityTasks, "Pushing entities");
411
+ // 3. Integration upserts — reference parameters / entities. Must be
412
+ // pushed BEFORE knowledge-base uploads (KBs attach to integrations)
413
+ // AND before tools (tools reference integration codes).
414
+ await pushStep(integrationUpsertTasks, "Pushing integrations");
415
+ // 4. Knowledge Bases — reference integrations existing on the platform.
416
+ // KB failures are folded into the same `failed` counter / pushFailures
417
+ // array as agent-settings writes, so they hit the "X failed." line, the
418
+ // JSON envelope, and the non-zero exit code (BFF contract: zero silent
419
+ // errors anywhere in the push pipeline).
420
+ const kbResult = await pushKnowledgeHubs(projectRoot, projectConfig);
421
+ if (!kbResult.ok) {
422
+ for (const kbFailure of kbResult.failures) {
423
+ failed++;
424
+ pushFailures.push(kbFailure);
425
+ }
426
+ }
427
+ // 5. Tools — reference parameters, entities, integrations. Sequential:
428
+ // parallel tool pushes caused inter-tool race conditions in
429
+ // production (chain-activation, success-rule re-sequencing).
430
+ await pushStep(toolTasks, "Pushing tools");
431
+ // 6. Rules — reference tools, parameters, entities.
432
+ await pushStep(rulesTasks, "Pushing rules");
433
+ // 7. General settings — mostly standalone but may reference defaults.
434
+ await pushStep(settingsTasks, "Pushing general settings");
435
+ // Phase 2: DELETES, bottom-up. Integrations get deleted last so any
436
+ // tool / entity update above that still referenced them succeeds first.
437
+ await pushStep(integrationDeleteTasks, "Removing integrations");
438
+ // Phase 3: Snapshot — runs at the very end of `_push` (see below).
641
439
  // Auth fallback
642
440
  if (authFailed && authFailedTasks.length > 0 && !savedApiKey) {
643
441
  // The auth fallback prompts for an API key. In a non-TTY environment
@@ -795,12 +593,29 @@ async function _push(pushSpan, agentCode, options = {}) {
795
593
  // This ensures: if snapshot fails, user re-runs `aui push` to retry both
796
594
  // failed entity pushes AND the snapshot. Local files remain the source
797
595
  // of truth until the server has captured them.
596
+ //
597
+ // CRITICAL (CTS-12340 follow-up): when one file has BOTH succeeded and
598
+ // failed tasks (e.g. integrations.aui.json with a successful DELETE on
599
+ // web-search and a failed POST on search-flights), do NOT commit that
600
+ // file to baseline. If we did, the next push's git diff would treat the
601
+ // failed item as "already on the platform" and emit a PATCH that 404s.
602
+ // The previous behaviour stuck users in an unrecoverable retry loop.
798
603
  let baselineUpdated = false;
799
604
  const canCommitBaseline = !prePushDraft || snapshotSucceeded;
800
605
  if (canCommitBaseline) {
606
+ // A file is committable iff EVERY task that targeted it succeeded.
607
+ // Build the failed-files set from `pushFailures` (which now includes
608
+ // both agent-settings entity failures AND knowledge-hub failures —
609
+ // see the KB push step).
610
+ const failedFiles = new Set();
611
+ for (const f of pushFailures) {
612
+ if (f.file)
613
+ failedFiles.add(f.file);
614
+ }
615
+ const filesSafeToCommit = succeededFiles.filter((f) => !failedFiles.has(f));
801
616
  if (failed > 0 && succeeded > 0) {
802
- if (succeededFiles.length > 0) {
803
- commitBaselineFiles(projectRoot, succeededFiles, `pushed ${succeeded} change(s)`);
617
+ if (filesSafeToCommit.length > 0) {
618
+ commitBaselineFiles(projectRoot, filesSafeToCommit, `pushed ${succeeded} change(s) (${failedFiles.size} file(s) held back due to per-task failures)`);
804
619
  baselineUpdated = true;
805
620
  }
806
621
  }
@@ -902,13 +717,11 @@ async function _push(pushSpan, agentCode, options = {}) {
902
717
  throw error;
903
718
  }
904
719
  }
905
- /**
906
- * Lookup the agent-management record for the current `.auirc` project
907
- * (preferred) or the active session fallbacksame precedence as draft
908
- * resolution. Each attempt records its error so callers can surface the full
909
- * picture instead of silently dropping `agent_id` from request bodies.
910
- */
911
- async function lookupAgentManagementInfoForPush(config, projectConfig, session) {
720
+ async function resolveVersionDraft(config, projectConfig, session, explicitVersionId) {
721
+ // Every error path below MUST throw a typed CLIError (not return null).
722
+ // Returning null silently exits the CLI with code 0 the BFF then thinks
723
+ // the push succeeded when nothing actually happened, and the failure
724
+ // never reaches Logfire because no exception bubbled to handleError.
912
725
  const client = new AUIClient({
913
726
  baseUrl: config.apiUrl,
914
727
  authToken: config.authToken,
@@ -920,98 +733,55 @@ async function lookupAgentManagementInfoForPush(config, projectConfig, session)
920
733
  if (key)
921
734
  client.setAgentSettingsApiKey(key);
922
735
  let agentInfo;
923
- const errors = [];
924
736
  const agentMgmtId = session.agent_management_id;
737
+ // Project's network_id (from .auirc) takes priority over session — when
738
+ // you're inside a project, that's the agent you mean. Session agent may
739
+ // point at a different agent (e.g. last `aui agents --switch`).
925
740
  const projectNetworkId = projectConfig.agent_id;
926
741
  const fallbackNetworkId = session.network_id;
927
742
  if (projectNetworkId) {
928
743
  try {
929
744
  const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: projectNetworkId });
930
- agentInfo = resp.items.find((a) => a.scope.network_id === projectNetworkId ||
931
- a.id === projectNetworkId);
932
- if (!agentInfo) {
933
- errors.push(`listAgents(network_id=${projectNetworkId}) returned ${resp.items.length} item(s), none matched.`);
934
- }
745
+ agentInfo = resp.items.find((a) => a.scope.network_id === projectNetworkId || a.id === projectNetworkId);
935
746
  }
936
747
  catch (err) {
937
- errors.push(`listAgents(network_id=${projectNetworkId}) threw: ${err instanceof Error ? err.message : String(err)}`);
748
+ // Listing fall-through is fine because the next two branches try other
749
+ // resolution paths AND a final ConfigError is thrown below if none
750
+ // succeed. But emit a debug warning so an operator with AUI_DEBUG=1
751
+ // can see WHICH branch failed and why (zero silent errors policy).
752
+ if (process.env.AUI_DEBUG) {
753
+ console.warn(`[debug] resolveVersionDraft: listAgents(network_id=${projectNetworkId}) failed:`, err instanceof Error ? err.message : err);
754
+ }
938
755
  }
939
756
  }
940
- // Try the session's agent_management_id even when the project has a network
941
- // id it's a direct getAgent call, no list scan, and it gracefully covers
942
- // the case where listAgents fell through above.
943
- if (!agentInfo && agentMgmtId) {
757
+ // Fall back to session's agent_management_id only when not inside a project
758
+ if (!agentInfo && !projectNetworkId && agentMgmtId) {
944
759
  try {
945
760
  agentInfo = await client.agentManagement.getAgent(agentMgmtId);
946
761
  }
947
762
  catch (err) {
948
- errors.push(`getAgent(${agentMgmtId}) threw: ${err instanceof Error ? err.message : String(err)}`);
763
+ if (process.env.AUI_DEBUG) {
764
+ console.warn(`[debug] resolveVersionDraft: getAgent(${agentMgmtId}) failed (stale id?):`, err instanceof Error ? err.message : err);
765
+ }
949
766
  }
950
767
  }
951
- if (!agentInfo && fallbackNetworkId && fallbackNetworkId !== projectNetworkId) {
768
+ // Last resort: session's network_id
769
+ if (!agentInfo && fallbackNetworkId) {
952
770
  try {
953
771
  const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: fallbackNetworkId });
954
- agentInfo = resp.items.find((a) => a.scope.network_id === fallbackNetworkId ||
955
- a.id === fallbackNetworkId);
956
- if (!agentInfo) {
957
- errors.push(`listAgents(network_id=${fallbackNetworkId}) returned ${resp.items.length} item(s), none matched.`);
958
- }
772
+ agentInfo = resp.items.find((a) => a.scope.network_id === fallbackNetworkId || a.id === fallbackNetworkId);
959
773
  }
960
774
  catch (err) {
961
- errors.push(`listAgents(network_id=${fallbackNetworkId}) threw: ${err instanceof Error ? err.message : String(err)}`);
775
+ if (process.env.AUI_DEBUG) {
776
+ console.warn(`[debug] resolveVersionDraft: listAgents(network_id=${fallbackNetworkId}) failed:`, err instanceof Error ? err.message : err);
777
+ }
962
778
  }
963
779
  }
964
- return { agentInfo, errors };
965
- }
966
- /**
967
- * Return the agent-management UUID to send as `agent_id` on agent-settings
968
- * write bodies. Reads `.auirc` first; falls back to `lookupAgentManagementInfoForPush`
969
- * and **persists** the resolved id back to `.auirc` so subsequent pushes don't
970
- * pay the lookup cost. Throws `ConfigError` if no id can be resolved — never
971
- * silently returns undefined, because that's how entities ended up in the DB
972
- * without `agent_id`.
973
- */
974
- async function resolvePushAgentManagementId(config, projectConfig, session, projectRoot) {
975
- if (projectConfig.agent_management_id)
976
- return projectConfig.agent_management_id;
977
- const { agentInfo, errors } = await lookupAgentManagementInfoForPush(config, projectConfig, session);
978
- if (!agentInfo) {
979
- const detail = errors.length > 0 ? `\n - ${errors.join("\n - ")}` : "";
980
- throw new ConfigError(`Could not resolve agent-management id for this project.${detail}`, {
981
- suggestion: "Re-run `aui import-agent` (will populate .auirc.agent_management_id) or `aui pull` to back-fill it.",
982
- });
983
- }
984
- // Migrate legacy projects: persist back so the next push skips the lookup.
985
- try {
986
- saveProjectConfig({ ...projectConfig, agent_management_id: agentInfo.id }, projectRoot);
987
- }
988
- catch {
989
- // .auirc write failure is non-fatal — we already have the id in memory.
990
- }
991
- return agentInfo.id;
992
- }
993
- async function resolveVersionDraft(config, projectConfig, session, explicitVersionId) {
994
- // Every error path below MUST throw a typed CLIError (not return null).
995
- // Returning null silently exits the CLI with code 0 — the BFF then thinks
996
- // the push succeeded when nothing actually happened, and the failure
997
- // never reaches Logfire because no exception bubbled to handleError.
998
- const { agentInfo, errors: lookupErrors } = await lookupAgentManagementInfoForPush(config, projectConfig, session);
999
780
  if (!agentInfo) {
1000
- const detail = lookupErrors.length > 0 ? `\n - ${lookupErrors.join("\n - ")}` : "";
1001
- throw new ConfigError(`Could not resolve agent for version management.${detail}`, {
781
+ throw new ConfigError("Could not resolve agent for version management.", {
1002
782
  suggestion: "Run `aui import-agent` to link an agent, or check your session with `aui status`.",
1003
783
  });
1004
784
  }
1005
- const client = new AUIClient({
1006
- baseUrl: config.apiUrl,
1007
- authToken: config.authToken,
1008
- accountId: config.accountId,
1009
- organizationId: config.organizationId,
1010
- environment: config.environment,
1011
- });
1012
- const key = loadAgentSettingsApiKey();
1013
- if (key)
1014
- client.setAgentSettingsApiKey(key);
1015
785
  // If user passed --version-id, validate it's a draft
1016
786
  if (explicitVersionId) {
1017
787
  let ver;
@@ -1146,8 +916,14 @@ async function resolveAgentSettingsParams(config, projectConfig, session, projec
1146
916
  saveProjectConfig({ ...projectConfig, network_category_id: categoryId }, projectRoot);
1147
917
  }
1148
918
  }
1149
- catch {
1150
- // ignore fetch failure
919
+ catch (err) {
920
+ // Falls through to the explicit ConfigError below if no categoryId
921
+ // could be resolved. Surface in AUI_DEBUG so the operator can see
922
+ // why the auto-fetch failed instead of just the generic "Missing
923
+ // network_category_id" message.
924
+ if (process.env.AUI_DEBUG) {
925
+ console.warn(`[debug] resolveAgentSettingsParams: networks.get(${networkId}) failed:`, err instanceof Error ? err.message : err);
926
+ }
1151
927
  }
1152
928
  }
1153
929
  if (!categoryId) {
@@ -1182,6 +958,215 @@ async function resolveAgentSettingsParams(config, projectConfig, session, projec
1182
958
  }
1183
959
  return baseParams;
1184
960
  }
961
+ // ─── Push Task Classification Helpers ───
962
+ /**
963
+ * Integration tasks split into two phases by the unified push order:
964
+ * - Upserts (POST/PATCH/PUT) run BEFORE knowledge bases + tools, so KBs
965
+ * can attach to integrations and tools can reference integration codes.
966
+ * - Deletes run AFTER tools / entities, so the last write that mentioned
967
+ * the integration succeeds before the row is removed.
968
+ */
969
+ function isIntegrationUpsertTask(t) {
970
+ return (t.type === "put-integrations" ||
971
+ t.type === "create-integration" ||
972
+ t.type === "patch-integration");
973
+ }
974
+ async function pushKnowledgeHubs(projectRoot, projectConfig) {
975
+ const { getKnowledgeHubChanges } = await import("../utils/git.js");
976
+ const kbChanges = getKnowledgeHubChanges(projectRoot);
977
+ if (kbChanges.length === 0)
978
+ return { ok: true, failures: [] };
979
+ const kbConfig = getConfig();
980
+ const kbSession = await getValidSession();
981
+ const kbNetworkId = projectConfig.agent_id || kbSession?.network_id;
982
+ if (!kbNetworkId || !kbConfig.authToken) {
983
+ return {
984
+ ok: false,
985
+ failures: [
986
+ {
987
+ label: "Push knowledge hubs",
988
+ error: "Cannot push knowledge hubs: missing network_id or auth token. Re-run `aui login` and `aui import-agent`.",
989
+ },
990
+ ],
991
+ };
992
+ }
993
+ const { KBViewClient } = await import("../api-client/kb-view-client.js");
994
+ const { buildScope, readKbFolder } = await import("../services/kb-view.service.js");
995
+ const { loadAgentSettingsApiKey: loadAsKey } = await import("../config/index.js");
996
+ const kbViewClient = new KBViewClient({
997
+ authToken: kbConfig.authToken,
998
+ apiKey: loadAsKey() || undefined,
999
+ organizationId: kbConfig.organizationId || "",
1000
+ environment: kbConfig.environment || "staging",
1001
+ });
1002
+ const kbLogDir = path.join(projectRoot, ".aui", "push-logs");
1003
+ fs.mkdirSync(kbLogDir, { recursive: true });
1004
+ kbViewClient.setPushLogDir(kbLogDir);
1005
+ const scope = buildScope({
1006
+ networkId: kbNetworkId,
1007
+ organizationId: projectConfig.organization_id || kbConfig.organizationId || "",
1008
+ accountId: projectConfig.account_id || kbConfig.accountId || "",
1009
+ });
1010
+ const userId = kbSession?.user_id || "cli";
1011
+ const changedKBDirs = new Set();
1012
+ for (const change of kbChanges) {
1013
+ if (change.kbDirName)
1014
+ changedKBDirs.add(change.kbDirName);
1015
+ }
1016
+ const existingKBDirs = [...changedKBDirs].filter((d) => fs.existsSync(path.join(projectRoot, "knowledge-hubs", d)));
1017
+ const deletedKBDirs = [...changedKBDirs].filter((d) => !fs.existsSync(path.join(projectRoot, "knowledge-hubs", d)));
1018
+ const failures = [];
1019
+ let kbDeleteSucceeded = true;
1020
+ if (deletedKBDirs.length > 0) {
1021
+ const { getBaselineFileContent } = await import("../utils/git.js");
1022
+ const deleteSpinner = startSpinner(`Deleting ${deletedKBDirs.length} knowledge base(s) from server...`);
1023
+ try {
1024
+ for (const kbDirName of deletedKBDirs) {
1025
+ const baselineKb = getBaselineFileContent(projectRoot, `knowledge-hubs/${kbDirName}/kb.json`);
1026
+ const kbName = baselineKb?.name || kbDirName;
1027
+ const kbId = baselineKb?.knowledge_base_id;
1028
+ if (!kbId) {
1029
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "warning", label: `Cannot delete "${kbName}" — no knowledge_base_id stored. Push the KB first, then delete.` }) }));
1030
+ continue;
1031
+ }
1032
+ try {
1033
+ await kbViewClient.deleteKnowledgeBase(kbId, scope, kbName);
1034
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: `Deleted: ${kbName}` }) }));
1035
+ }
1036
+ catch (delErr) {
1037
+ // Per-KB error: count it, keep going so partial work shows up.
1038
+ if (isNotFoundError(delErr)) {
1039
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: `Deleted: ${kbName} (already absent)` }) }));
1040
+ }
1041
+ else {
1042
+ kbDeleteSucceeded = false;
1043
+ const errMsg = delErr instanceof Error ? delErr.message : String(delErr);
1044
+ failures.push({
1045
+ label: `Delete knowledge base: ${kbName}`,
1046
+ file: `knowledge-hubs/${kbDirName}/kb.json`,
1047
+ error: errMsg,
1048
+ });
1049
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "error", label: `Failed to delete "${kbName}": ${errMsg}` }) }));
1050
+ }
1051
+ }
1052
+ }
1053
+ if (kbDeleteSucceeded) {
1054
+ deleteSpinner.succeed(`${deletedKBDirs.length} knowledge base(s) deleted`);
1055
+ }
1056
+ else {
1057
+ deleteSpinner.fail(`Knowledge base deletion completed with errors`);
1058
+ }
1059
+ }
1060
+ catch (error) {
1061
+ kbDeleteSucceeded = false;
1062
+ deleteSpinner.fail("Knowledge base deletion failed");
1063
+ const errMsg = error instanceof Error ? error.message : String(error);
1064
+ failures.push({
1065
+ label: "Delete knowledge bases (batch)",
1066
+ error: errMsg,
1067
+ });
1068
+ log(_jsx(ErrorDisplay, { error: error }));
1069
+ }
1070
+ }
1071
+ let kbUploadSucceeded = false;
1072
+ if (existingKBDirs.length > 0) {
1073
+ const kbSpinner = startSpinner(`Pushing ${existingKBDirs.length} knowledge base(s)...`);
1074
+ let hadUploadFailure = false;
1075
+ try {
1076
+ for (const kbDirName of existingKBDirs) {
1077
+ const kbDir = path.join(projectRoot, "knowledge-hubs", kbDirName);
1078
+ const kbData = readKbFolder(kbDir);
1079
+ if (!kbData)
1080
+ continue;
1081
+ const SUPPORTED_EXTENSIONS = new Set([".pdf", ".md", ".txt", ".json"]);
1082
+ const supportedFiles = kbData.binaryFiles.filter((f) => SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
1083
+ const skippedFiles = kbData.binaryFiles.filter((f) => !SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
1084
+ for (const skipped of skippedFiles) {
1085
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "warning", label: `Skipped unsupported file: ${path.basename(skipped)} (only .pdf, .md, .txt, .json)` }) }));
1086
+ }
1087
+ if (supportedFiles.length > 0) {
1088
+ try {
1089
+ const importResult = await kbViewClient.importFiles({
1090
+ files: supportedFiles,
1091
+ scope,
1092
+ created_by: userId,
1093
+ knowledge_base_name: kbData.name,
1094
+ knowledge_base_description: kbData.description,
1095
+ });
1096
+ if (importResult.knowledge_base_id) {
1097
+ const kbJsonPath = path.join(kbDir, "kb.json");
1098
+ try {
1099
+ const raw = JSON.parse(fs.readFileSync(kbJsonPath, "utf-8"));
1100
+ raw.knowledge_base_id = importResult.knowledge_base_id;
1101
+ fs.writeFileSync(kbJsonPath, JSON.stringify(raw, null, 2) + "\n");
1102
+ }
1103
+ catch (writeErr) {
1104
+ // kb.json id write fail is non-fatal but tell the user so the
1105
+ // next push doesn't surprise them with "no knowledge_base_id stored".
1106
+ if (process.env.AUI_DEBUG) {
1107
+ console.warn(`[debug] failed to write knowledge_base_id back to ${kbJsonPath}:`, writeErr);
1108
+ }
1109
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "warning", label: `Could not persist knowledge_base_id back to ${path.basename(kbJsonPath)} — re-import or run \`aui pull\` to recover.` }) }));
1110
+ }
1111
+ }
1112
+ }
1113
+ catch (uploadErr) {
1114
+ hadUploadFailure = true;
1115
+ const errMsg = uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
1116
+ failures.push({
1117
+ label: `Push knowledge base: ${kbData.name || kbDirName}`,
1118
+ file: `knowledge-hubs/${kbDirName}/kb.json`,
1119
+ error: errMsg,
1120
+ });
1121
+ log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "error", label: `Failed to push "${kbData.name || kbDirName}": ${errMsg}` }) }));
1122
+ }
1123
+ }
1124
+ }
1125
+ if (hadUploadFailure) {
1126
+ kbSpinner.fail(`Knowledge base push completed with errors`);
1127
+ kbUploadSucceeded = false;
1128
+ }
1129
+ else {
1130
+ kbSpinner.succeed(`Knowledge base(s) pushed`);
1131
+ kbUploadSucceeded = true;
1132
+ }
1133
+ }
1134
+ catch (error) {
1135
+ kbSpinner.fail("Knowledge base push failed");
1136
+ const errMsg = error instanceof Error ? error.message : String(error);
1137
+ failures.push({
1138
+ label: "Push knowledge bases (batch)",
1139
+ error: errMsg,
1140
+ });
1141
+ log(_jsx(ErrorDisplay, { error: error }));
1142
+ }
1143
+ }
1144
+ else {
1145
+ kbUploadSucceeded = true;
1146
+ }
1147
+ const kbPushSucceeded = kbUploadSucceeded && kbDeleteSucceeded;
1148
+ if (kbPushSucceeded) {
1149
+ const kbFilesToAdd = kbChanges
1150
+ .filter((c) => c.status !== "deleted")
1151
+ .map((c) => c.file);
1152
+ const kbFilesToDelete = kbChanges
1153
+ .filter((c) => c.status === "deleted")
1154
+ .map((c) => c.file);
1155
+ if (kbFilesToAdd.length > 0 || kbFilesToDelete.length > 0) {
1156
+ const { commitBaselineFiles: commitKBFiles, removeBaselineFiles } = await import("../utils/git.js");
1157
+ if (kbFilesToDelete.length > 0) {
1158
+ removeBaselineFiles(projectRoot, kbFilesToDelete);
1159
+ }
1160
+ if (kbFilesToAdd.length > 0) {
1161
+ commitKBFiles(projectRoot, kbFilesToAdd, "pushed knowledge hub changes");
1162
+ }
1163
+ else {
1164
+ commitKBFiles(projectRoot, [], "removed knowledge hub files");
1165
+ }
1166
+ }
1167
+ }
1168
+ return { ok: kbPushSucceeded && failures.length === 0, failures };
1169
+ }
1185
1170
  // ─── Array File Info Helper ───
1186
1171
  function getArrayFileInfoForPush(filePath, dir) {
1187
1172
  try {
@@ -1210,11 +1195,6 @@ function getArrayFileInfoForPush(filePath, dir) {
1210
1195
  return null;
1211
1196
  }
1212
1197
  }
1213
- function isIntegrationUpsertTask(t) {
1214
- return (t.type === "put-integrations" ||
1215
- t.type === "create-integration" ||
1216
- t.type === "patch-integration");
1217
- }
1218
1198
  function writePushMemory(projectRoot, agentCode, agentId, pushTasks, succeededFiles, pushFailures) {
1219
1199
  try {
1220
1200
  const memoryDir = path.join(projectRoot, "memory");
@@ -1304,7 +1284,13 @@ function writePushMemory(projectRoot, agentCode, agentId, pushTasks, succeededFi
1304
1284
  fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
1305
1285
  return path.relative(projectRoot, filePath);
1306
1286
  }
1307
- catch {
1287
+ catch (err) {
1288
+ // Memory file is diagnostic only — its failure shouldn't block the push.
1289
+ // But emit a debug warning so an operator chasing "where's my push memory"
1290
+ // sees what went wrong.
1291
+ if (process.env.AUI_DEBUG) {
1292
+ console.warn("[debug] writePushMemory failed:", err instanceof Error ? err.message : err);
1293
+ }
1308
1294
  return undefined;
1309
1295
  }
1310
1296
  }
@@ -1594,95 +1580,280 @@ async function executePushTask(client, params, task) {
1594
1580
  }
1595
1581
  });
1596
1582
  }
1597
- // Fall back to PATCH only on a 409 Conflict from the create call — the BFF
1598
- // returns 409 specifically for "already exists" conflicts, so any other
1599
- // status code is a real error and must be surfaced (not masked behind a
1600
- // follow-up PATCH that would likely fail with a misleading "not found").
1583
+ // ─── Adaptive fallback matrix (per write task) ────────────────────────────
1584
+ //
1585
+ // Every entity write goes through three layers, applied in this order:
1586
+ //
1587
+ // (a) `withTransientRetry` — retries once on 500/502/503/504 with a 1s
1588
+ // back-off. Per-call, isolated from other
1589
+ // tasks. 4xx is never retried (deterministic).
1590
+ // (b) `POST 409 → PATCH` — the create call hit a row with the same
1591
+ // code; the platform already has it. Convert
1592
+ // to a PATCH and continue. Pre-existing.
1593
+ // (c) `PATCH 404 → POST` — the patch call hit "not found"; baseline
1594
+ // drifted (item never landed on the platform
1595
+ // from a prior partial push). Convert to a
1596
+ // POST so the row reappears. NEW.
1597
+ // (d) `DELETE 404 → success` — the delete target is already gone. The
1598
+ // desired end state is reached. Treat as
1599
+ // success and log "(already absent)" so the
1600
+ // user can see what happened. NEW.
1601
+ //
1602
+ // All four layers are visible in the per-task push log files under
1603
+ // `.aui/push-logs/` so the BFF / agent-builder-bff can audit decisions.
1601
1604
  function isAlreadyExistsConflict(err) {
1602
1605
  if (!err || typeof err !== "object")
1603
1606
  return false;
1604
- const statusCode = err.statusCode
1607
+ const code = err.statusCode
1608
+ ?? err.status;
1609
+ return code === 409;
1610
+ }
1611
+ function isNotFoundError(err) {
1612
+ if (!err || typeof err !== "object")
1613
+ return false;
1614
+ const code = err.statusCode
1605
1615
  ?? err.status;
1606
- return statusCode === 409;
1616
+ return code === 404;
1617
+ }
1618
+ function isTransient5xx(err) {
1619
+ if (!err || typeof err !== "object")
1620
+ return false;
1621
+ const code = err.statusCode
1622
+ ?? err.status;
1623
+ return code === 500 || code === 502 || code === 503 || code === 504;
1624
+ }
1625
+ /**
1626
+ * Run one entity-settings write call once, and retry exactly once on a
1627
+ * transient 5xx after a 1s back-off. The snapshot upload has its own
1628
+ * retry loop (see `pushSnapshot`); this is the equivalent for individual
1629
+ * agent-settings writes. Never retries on 4xx — those are deterministic.
1630
+ */
1631
+ async function withTransientRetry(label, fn) {
1632
+ try {
1633
+ return await fn();
1634
+ }
1635
+ catch (err) {
1636
+ if (!isTransient5xx(err))
1637
+ throw err;
1638
+ const code = err.statusCode
1639
+ ?? err.status;
1640
+ if (process.env.AUI_DEBUG) {
1641
+ console.log(`[debug] ${label} got ${code}, retrying once after 1000ms`);
1642
+ }
1643
+ await new Promise((r) => setTimeout(r, 1000));
1644
+ return await fn();
1645
+ }
1646
+ }
1647
+ /**
1648
+ * A delete that has been short-circuited because the row was already absent
1649
+ * on the platform. Returned as a successful resolution so callers don't
1650
+ * count the task as failed, but tagged so the per-task log line can show
1651
+ * "(already absent)" instead of a generic ✓.
1652
+ */
1653
+ const DELETE_ALREADY_ABSENT = Object.freeze({
1654
+ __aui_already_absent__: true,
1655
+ message: "Already absent on platform — treated as success",
1656
+ });
1657
+ function isAlreadyAbsentResult(value) {
1658
+ return (!!value
1659
+ && typeof value === "object"
1660
+ && value.__aui_already_absent__ === true);
1607
1661
  }
1608
1662
  async function _executePushTask(client, params, task) {
1609
1663
  switch (task.type) {
1610
1664
  case "patch-tool":
1611
- return client.patchTool(params, task.toolName, task.body);
1612
- case "create-tool":
1613
- try {
1614
- return await client.createTool(params, task.body);
1615
- }
1616
- catch (err) {
1617
- if (isAlreadyExistsConflict(err)) {
1618
- if (process.env.AUI_DEBUG) {
1619
- console.log(`[debug] create-tool: already-exists detected, falling back to PATCH`);
1665
+ return withTransientRetry(`PATCH tool ${task.toolName}`, async () => {
1666
+ try {
1667
+ return await client.patchTool(params, task.toolName, task.body);
1668
+ }
1669
+ catch (err) {
1670
+ if (isNotFoundError(err)) {
1671
+ if (process.env.AUI_DEBUG) {
1672
+ console.log(`[debug] patch-tool ${task.toolName}: 404 not found, falling back to POST`);
1673
+ }
1674
+ return client.createTool(params, task.body);
1620
1675
  }
1621
- const body = task.body;
1622
- const toolCode = body.code || "";
1623
- const toolName = toolCode.toUpperCase().replace(/-/g, "_");
1624
- return client.patchTool(params, toolName, body);
1676
+ throw err;
1625
1677
  }
1626
- if (process.env.AUI_DEBUG) {
1627
- const statusCode = err.statusCode ?? err.status;
1628
- console.log(`[debug] create-tool failed (${statusCode}); not already-exists, surfacing original`);
1678
+ });
1679
+ case "create-tool":
1680
+ return withTransientRetry(`POST tool ${task.toolName ?? task.itemCode}`, async () => {
1681
+ try {
1682
+ return await client.createTool(params, task.body);
1629
1683
  }
1630
- throw err;
1631
- }
1684
+ catch (err) {
1685
+ if (isAlreadyExistsConflict(err)) {
1686
+ if (process.env.AUI_DEBUG) {
1687
+ console.log(`[debug] create-tool: 409 already-exists, falling back to PATCH`);
1688
+ }
1689
+ const body = task.body;
1690
+ const toolCode = body.code || "";
1691
+ const toolName = toolCode.toUpperCase().replace(/-/g, "_");
1692
+ return client.patchTool(params, toolName, body);
1693
+ }
1694
+ throw err;
1695
+ }
1696
+ });
1632
1697
  case "delete-tool":
1633
- return client.deleteTool(params, task.toolName);
1698
+ return withTransientRetry(`DELETE tool ${task.toolName}`, async () => {
1699
+ try {
1700
+ return await client.deleteTool(params, task.toolName);
1701
+ }
1702
+ catch (err) {
1703
+ if (isNotFoundError(err)) {
1704
+ if (process.env.AUI_DEBUG) {
1705
+ console.log(`[debug] delete-tool ${task.toolName}: 404 already absent`);
1706
+ }
1707
+ return DELETE_ALREADY_ABSENT;
1708
+ }
1709
+ throw err;
1710
+ }
1711
+ });
1634
1712
  case "patch-general-settings":
1635
- return client.patchGeneralSettings(params, task.body);
1713
+ return withTransientRetry("PATCH general-settings", () => client.patchGeneralSettings(params, task.body));
1636
1714
  case "put-parameters":
1637
- return client.putParameters(params, task.body, task.oldBody);
1715
+ return withTransientRetry("PUT parameters", () => client.putParameters(params, task.body, task.oldBody));
1638
1716
  case "put-entities":
1639
- return client.putEntities(params, task.body, task.oldBody);
1717
+ return withTransientRetry("PUT entities", () => client.putEntities(params, task.body, task.oldBody));
1640
1718
  case "put-integrations":
1641
- return client.putIntegrations(params, task.body, task.oldBody);
1719
+ return withTransientRetry("PUT integrations", () => client.putIntegrations(params, task.body, task.oldBody));
1642
1720
  case "create-parameter":
1643
- try {
1644
- return await client.createParameter(params, task.body);
1645
- }
1646
- catch (err) {
1647
- if (isAlreadyExistsConflict(err)) {
1648
- return client.patchParameter(params, task.itemCode, task.body);
1721
+ return withTransientRetry(`POST param ${task.itemCode}`, async () => {
1722
+ try {
1723
+ return await client.createParameter(params, task.body);
1649
1724
  }
1650
- throw err;
1651
- }
1725
+ catch (err) {
1726
+ if (isAlreadyExistsConflict(err)) {
1727
+ if (process.env.AUI_DEBUG) {
1728
+ console.log(`[debug] create-parameter ${task.itemCode}: 409, falling back to PATCH`);
1729
+ }
1730
+ return client.patchParameter(params, task.itemCode, task.body);
1731
+ }
1732
+ throw err;
1733
+ }
1734
+ });
1652
1735
  case "patch-parameter":
1653
- return client.patchParameter(params, task.itemCode, task.body);
1736
+ return withTransientRetry(`PATCH param ${task.itemCode}`, async () => {
1737
+ try {
1738
+ return await client.patchParameter(params, task.itemCode, task.body);
1739
+ }
1740
+ catch (err) {
1741
+ if (isNotFoundError(err)) {
1742
+ if (process.env.AUI_DEBUG) {
1743
+ console.log(`[debug] patch-parameter ${task.itemCode}: 404 not found, falling back to POST`);
1744
+ }
1745
+ return client.createParameter(params, task.body);
1746
+ }
1747
+ throw err;
1748
+ }
1749
+ });
1654
1750
  case "delete-parameter":
1655
- return client.deleteParameter(params, task.itemCode, task.body);
1751
+ return withTransientRetry(`DELETE param ${task.itemCode}`, async () => {
1752
+ try {
1753
+ return await client.deleteParameter(params, task.itemCode, task.body);
1754
+ }
1755
+ catch (err) {
1756
+ if (isNotFoundError(err)) {
1757
+ if (process.env.AUI_DEBUG) {
1758
+ console.log(`[debug] delete-parameter ${task.itemCode}: 404 already absent`);
1759
+ }
1760
+ return DELETE_ALREADY_ABSENT;
1761
+ }
1762
+ throw err;
1763
+ }
1764
+ });
1656
1765
  case "create-entity":
1657
- try {
1658
- return await client.createEntity(params, task.body);
1659
- }
1660
- catch (err) {
1661
- if (isAlreadyExistsConflict(err)) {
1662
- return client.patchEntity(params, task.itemCode, task.body);
1766
+ return withTransientRetry(`POST entity ${task.itemCode}`, async () => {
1767
+ try {
1768
+ return await client.createEntity(params, task.body);
1663
1769
  }
1664
- throw err;
1665
- }
1770
+ catch (err) {
1771
+ if (isAlreadyExistsConflict(err)) {
1772
+ if (process.env.AUI_DEBUG) {
1773
+ console.log(`[debug] create-entity ${task.itemCode}: 409, falling back to PATCH`);
1774
+ }
1775
+ return client.patchEntity(params, task.itemCode, task.body);
1776
+ }
1777
+ throw err;
1778
+ }
1779
+ });
1666
1780
  case "patch-entity":
1667
- return client.patchEntity(params, task.itemCode, task.body);
1781
+ return withTransientRetry(`PATCH entity ${task.itemCode}`, async () => {
1782
+ try {
1783
+ return await client.patchEntity(params, task.itemCode, task.body);
1784
+ }
1785
+ catch (err) {
1786
+ if (isNotFoundError(err)) {
1787
+ if (process.env.AUI_DEBUG) {
1788
+ console.log(`[debug] patch-entity ${task.itemCode}: 404, falling back to POST`);
1789
+ }
1790
+ return client.createEntity(params, task.body);
1791
+ }
1792
+ throw err;
1793
+ }
1794
+ });
1668
1795
  case "delete-entity":
1669
- return client.deleteEntity(params, task.itemCode);
1796
+ return withTransientRetry(`DELETE entity ${task.itemCode}`, async () => {
1797
+ try {
1798
+ return await client.deleteEntity(params, task.itemCode);
1799
+ }
1800
+ catch (err) {
1801
+ if (isNotFoundError(err)) {
1802
+ if (process.env.AUI_DEBUG) {
1803
+ console.log(`[debug] delete-entity ${task.itemCode}: 404 already absent`);
1804
+ }
1805
+ return DELETE_ALREADY_ABSENT;
1806
+ }
1807
+ throw err;
1808
+ }
1809
+ });
1670
1810
  case "create-integration":
1671
- try {
1672
- return await client.createIntegration(params, task.body);
1673
- }
1674
- catch (err) {
1675
- if (isAlreadyExistsConflict(err)) {
1676
- return client.patchIntegration(params, task.itemCode, task.body);
1811
+ return withTransientRetry(`POST integration ${task.itemCode}`, async () => {
1812
+ try {
1813
+ return await client.createIntegration(params, task.body);
1677
1814
  }
1678
- throw err;
1679
- }
1815
+ catch (err) {
1816
+ if (isAlreadyExistsConflict(err)) {
1817
+ if (process.env.AUI_DEBUG) {
1818
+ console.log(`[debug] create-integration ${task.itemCode}: 409, falling back to PATCH`);
1819
+ }
1820
+ return client.patchIntegration(params, task.itemCode, task.body);
1821
+ }
1822
+ throw err;
1823
+ }
1824
+ });
1680
1825
  case "patch-integration":
1681
- return client.patchIntegration(params, task.itemCode, task.body);
1826
+ return withTransientRetry(`PATCH integration ${task.itemCode}`, async () => {
1827
+ try {
1828
+ return await client.patchIntegration(params, task.itemCode, task.body);
1829
+ }
1830
+ catch (err) {
1831
+ if (isNotFoundError(err)) {
1832
+ if (process.env.AUI_DEBUG) {
1833
+ console.log(`[debug] patch-integration ${task.itemCode}: 404 not found, falling back to POST`);
1834
+ }
1835
+ return client.createIntegration(params, task.body);
1836
+ }
1837
+ throw err;
1838
+ }
1839
+ });
1682
1840
  case "delete-integration":
1683
- return client.deleteIntegration(params, task.itemCode);
1841
+ return withTransientRetry(`DELETE integration ${task.itemCode}`, async () => {
1842
+ try {
1843
+ return await client.deleteIntegration(params, task.itemCode);
1844
+ }
1845
+ catch (err) {
1846
+ if (isNotFoundError(err)) {
1847
+ if (process.env.AUI_DEBUG) {
1848
+ console.log(`[debug] delete-integration ${task.itemCode}: 404 already absent`);
1849
+ }
1850
+ return DELETE_ALREADY_ABSENT;
1851
+ }
1852
+ throw err;
1853
+ }
1854
+ });
1684
1855
  case "put-rules":
1685
- return client.putRules(params, task.body);
1856
+ return withTransientRetry("PUT rules", () => client.putRules(params, task.body));
1686
1857
  default:
1687
1858
  throw new Error(`Unknown push task type: ${task.type}`);
1688
1859
  }