aui-agent-builder 0.3.82 → 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,211 +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
- const ag = await lookupAgentManagementInfoForPush(config, projectConfig, intSession);
286
- cachedPushAgentManagementId = ag?.id;
287
- }
288
- if (cachedPushAgentManagementId) {
289
- intAsp.agent_id = cachedPushAgentManagementId;
290
- }
291
- }
292
- log(_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.info, children: [icons.bullet, " Pushing integrations (before KB upload)..."] }) }));
293
- for (const task of integrationUpsertTasksEarly) {
294
- const taskResult = {
295
- label: task.label,
296
- status: "success",
297
- };
298
- try {
299
- await executePushTask(intClient, intAsp, task);
300
- }
301
- catch (err) {
302
- const errMsg = err instanceof Error ? err.message : String(err);
303
- taskResult.status = "failed";
304
- taskResult.error = errMsg;
305
- }
306
- log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
307
- }
308
- integrationUpsertsAlreadyPushed = true;
309
- }
310
- }
311
- }
312
- }
313
- // ─── Knowledge Hubs Push (full files) ───
314
- const { getKnowledgeHubChanges } = await import("../utils/git.js");
315
- const kbChanges = getKnowledgeHubChanges(projectRoot);
316
- if (kbChanges.length > 0) {
317
- const kbConfig = getConfig();
318
- const kbSession = await getValidSession();
319
- const kbNetworkId = projectConfig.agent_id || kbSession?.network_id;
320
- if (kbNetworkId && kbConfig.authToken) {
321
- const { KBViewClient } = await import("../api-client/kb-view-client.js");
322
- const { buildScope, readKbFolder } = await import("../services/kb-view.service.js");
323
- const { loadAgentSettingsApiKey: loadAsKey } = await import("../config/index.js");
324
- const kbViewClient = new KBViewClient({
325
- authToken: kbConfig.authToken,
326
- apiKey: loadAsKey() || undefined,
327
- organizationId: kbConfig.organizationId || "",
328
- environment: kbConfig.environment || "staging",
329
- });
330
- const kbLogDir = path.join(projectRoot, ".aui", "push-logs");
331
- fs.mkdirSync(kbLogDir, { recursive: true });
332
- kbViewClient.setPushLogDir(kbLogDir);
333
- const scope = buildScope({
334
- networkId: kbNetworkId,
335
- organizationId: projectConfig.organization_id || kbConfig.organizationId || "",
336
- accountId: projectConfig.account_id || kbConfig.accountId || "",
337
- });
338
- const userId = kbSession?.user_id || "cli";
339
- // Collect all changed KB directories (skip root-level files)
340
- const changedKBDirs = new Set();
341
- for (const change of kbChanges) {
342
- if (change.kbDirName) {
343
- changedKBDirs.add(change.kbDirName);
344
- }
345
- }
346
- // Split into existing (will upload) and deleted (will delete from server)
347
- const existingKBDirs = [...changedKBDirs].filter((d) => fs.existsSync(path.join(projectRoot, "knowledge-hubs", d)));
348
- const deletedKBDirs = [...changedKBDirs].filter((d) => !fs.existsSync(path.join(projectRoot, "knowledge-hubs", d)));
349
- // Delete KBs that were removed locally
350
- let kbDeleteSucceeded = true;
351
- if (deletedKBDirs.length > 0) {
352
- const { getBaselineFileContent } = await import("../utils/git.js");
353
- const deleteSpinner = startSpinner(`Deleting ${deletedKBDirs.length} knowledge base(s) from server...`);
354
- try {
355
- for (const kbDirName of deletedKBDirs) {
356
- const baselineKb = getBaselineFileContent(projectRoot, `knowledge-hubs/${kbDirName}/kb.json`);
357
- const kbName = baselineKb?.name || kbDirName;
358
- const kbId = baselineKb?.knowledge_base_id;
359
- if (!kbId) {
360
- 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.` }) }));
361
- continue;
362
- }
363
- await kbViewClient.deleteKnowledgeBase(kbId, scope, kbName);
364
- log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: `Deleted: ${kbName}` }) }));
365
- }
366
- deleteSpinner.succeed(`${deletedKBDirs.length} knowledge base(s) deleted`);
367
- }
368
- catch (error) {
369
- kbDeleteSucceeded = false;
370
- deleteSpinner.fail("Knowledge base deletion failed");
371
- log(_jsx(ErrorDisplay, { error: error }));
372
- }
373
- }
374
- // Upload full files for each changed KB
375
- let kbUploadSucceeded = false;
376
- if (existingKBDirs.length > 0) {
377
- const kbSpinner = startSpinner(`Pushing ${existingKBDirs.length} knowledge base(s)...`);
378
- try {
379
- for (const kbDirName of existingKBDirs) {
380
- const kbDir = path.join(projectRoot, "knowledge-hubs", kbDirName);
381
- const kbData = readKbFolder(kbDir);
382
- if (!kbData)
383
- continue;
384
- const SUPPORTED_EXTENSIONS = new Set([".pdf", ".md", ".txt", ".json"]);
385
- const supportedFiles = kbData.binaryFiles.filter((f) => SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
386
- const skippedFiles = kbData.binaryFiles.filter((f) => !SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
387
- for (const skipped of skippedFiles) {
388
- log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "warning", label: `Skipped unsupported file: ${path.basename(skipped)} (only .pdf, .md, .txt, .json)` }) }));
389
- }
390
- if (supportedFiles.length > 0) {
391
- const importResult = await kbViewClient.importFiles({
392
- files: supportedFiles,
393
- scope,
394
- created_by: userId,
395
- knowledge_base_name: kbData.name,
396
- knowledge_base_description: kbData.description,
397
- });
398
- if (importResult.knowledge_base_id) {
399
- const kbJsonPath = path.join(kbDir, "kb.json");
400
- try {
401
- const raw = JSON.parse(fs.readFileSync(kbJsonPath, "utf-8"));
402
- raw.knowledge_base_id = importResult.knowledge_base_id;
403
- fs.writeFileSync(kbJsonPath, JSON.stringify(raw, null, 2) + "\n");
404
- }
405
- catch { /* kb.json write failed, non-fatal */ }
406
- }
407
- }
408
- }
409
- kbSpinner.succeed(`Knowledge base(s) pushed`);
410
- kbUploadSucceeded = true;
411
- }
412
- catch (error) {
413
- kbSpinner.fail("Knowledge base push failed");
414
- log(_jsx(ErrorDisplay, { error: error }));
415
- }
416
- }
417
- else {
418
- kbUploadSucceeded = true;
419
- }
420
- const kbPushSucceeded = kbUploadSucceeded && kbDeleteSucceeded;
421
- // Commit KB changes to baseline only if push succeeded
422
- if (kbPushSucceeded) {
423
- const kbFilesToAdd = kbChanges
424
- .filter((c) => c.status !== "deleted")
425
- .map((c) => c.file);
426
- const kbFilesToDelete = kbChanges
427
- .filter((c) => c.status === "deleted")
428
- .map((c) => c.file);
429
- if (kbFilesToAdd.length > 0 || kbFilesToDelete.length > 0) {
430
- const { commitBaselineFiles: commitKBFiles, removeBaselineFiles } = await import("../utils/git.js");
431
- if (kbFilesToDelete.length > 0) {
432
- removeBaselineFiles(projectRoot, kbFilesToDelete);
433
- }
434
- if (kbFilesToAdd.length > 0) {
435
- commitKBFiles(projectRoot, kbFilesToAdd, "pushed knowledge hub changes");
436
- }
437
- else {
438
- commitKBFiles(projectRoot, [], "removed knowledge hub files");
439
- }
440
- }
441
- }
442
- }
443
- }
444
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.
445
249
  if (!diff || !diff.hasChanges) {
446
250
  pushSpan.setAttribute("push.exit_reason", "no_agent_config_changes");
447
251
  log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: "No agent config changes to push." }) }));
@@ -482,23 +286,10 @@ async function _push(pushSpan, agentCode, options = {}) {
482
286
  // available drafts. Push is rejected if no draft is found.
483
287
  let prePushDraft = null;
484
288
  if (projectConfig.version_id || options.versionId) {
485
- prePushDraft =
486
- resolvedPushDraftCache ??
487
- (await resolveVersionDraft(config, projectConfig, session, options.versionId));
289
+ prePushDraft = await resolveVersionDraft(config, projectConfig, session, options.versionId);
488
290
  agentSettingsParams.version_id = prePushDraft.versionId;
489
- agentSettingsParams.agent_id = prePushDraft.agentId;
490
- cachedPushAgentManagementId = prePushDraft.agentId;
491
291
  log(_jsx(StatusLine, { kind: "info", label: `Pushing into draft version: ${prePushDraft.label}` }));
492
292
  }
493
- else {
494
- if (!cachedPushAgentManagementId) {
495
- const ag = await lookupAgentManagementInfoForPush(config, projectConfig, session);
496
- cachedPushAgentManagementId = ag?.id;
497
- }
498
- if (cachedPushAgentManagementId) {
499
- agentSettingsParams.agent_id = cachedPushAgentManagementId;
500
- }
501
- }
502
293
  const pushTasks = buildPushTasks(diff, fileData, projectRoot, getFileDiff);
503
294
  pushSpan.setAttribute("push.task_count", pushTasks.length);
504
295
  if (diff) {
@@ -532,82 +323,55 @@ async function _push(pushSpan, agentCode, options = {}) {
532
323
  };
533
324
  const agentCodeStr = projectConfig.agent_code || projectConfig.agent_id || "unknown";
534
325
  const agentIdStr = projectConfig.agent_id || "unknown";
535
- 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) => {
536
335
  if (tasks.length === 0)
537
336
  return true;
538
337
  log(_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.info, children: [icons.bullet, " ", label, "..."] }) }));
539
338
  const stepFailed = [];
540
339
  try {
541
- if (parallel) {
542
- const results = await Promise.allSettled(tasks.map((t) => executePushTask(client, agentSettingsParams, t)));
543
- for (let i = 0; i < results.length; i++) {
544
- const taskResult = {
545
- label: tasks[i].label,
546
- status: "success",
547
- };
548
- if (results[i].status === "fulfilled") {
549
- succeeded++;
550
- if (tasks[i].file)
551
- succeededFiles.push(tasks[i].file);
552
- }
553
- else {
554
- const err = results[i].reason;
555
- if (isAuthError(err)) {
556
- authFailed = true;
557
- authFailedTasks.push(tasks[i]);
558
- taskResult.status = "auth-failed";
559
- }
560
- else {
561
- failed++;
562
- const errMsg = err instanceof Error ? err.message : String(err);
563
- const failure = {
564
- label: tasks[i].label,
565
- file: tasks[i].file,
566
- error: errMsg,
567
- };
568
- pushFailures.push(failure);
569
- stepFailed.push(failure);
570
- taskResult.status = "failed";
571
- taskResult.error = errMsg;
572
- }
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)`;
573
352
  }
574
- log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
575
353
  }
576
- }
577
- else {
578
- for (const task of tasks) {
579
- const taskResult = {
580
- label: task.label,
581
- status: "success",
582
- };
583
- try {
584
- await executePushTask(client, agentSettingsParams, task);
585
- succeeded++;
586
- if (task.file)
587
- succeededFiles.push(task.file);
354
+ catch (err) {
355
+ if (isAuthError(err)) {
356
+ authFailed = true;
357
+ authFailedTasks.push(task);
358
+ taskResult.status = "auth-failed";
588
359
  }
589
- catch (err) {
590
- if (isAuthError(err)) {
591
- authFailed = true;
592
- authFailedTasks.push(task);
593
- taskResult.status = "auth-failed";
594
- }
595
- else {
596
- failed++;
597
- const errMsg = err instanceof Error ? err.message : String(err);
598
- const failure = {
599
- label: task.label,
600
- file: task.file,
601
- error: errMsg,
602
- };
603
- pushFailures.push(failure);
604
- stepFailed.push(failure);
605
- taskResult.status = "failed";
606
- taskResult.error = errMsg;
607
- }
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;
608
372
  }
609
- log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
610
373
  }
374
+ log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
611
375
  }
612
376
  return stepFailed.length === 0 && !authFailed;
613
377
  }
@@ -633,17 +397,45 @@ async function _push(pushSpan, agentCode, options = {}) {
633
397
  t.type === "delete-tool");
634
398
  const settingsTasks = pushTasks.filter((t) => t.type === "patch-general-settings");
635
399
  const rulesTasks = pushTasks.filter((t) => t.type === "put-rules");
636
- await pushStep(paramTasks, "Pushing parameters", false);
637
- // Integrations before entities so workflows/tools can safely reference integration codes.
638
- if (!integrationUpsertsAlreadyPushed) {
639
- await pushStep(integrationUpsertTasks, "Pushing integrations", false);
640
- }
641
- await pushStep(entityTasks, "Pushing entities", false);
642
- // Delete integrations after entity changes drop references where needed.
643
- await pushStep(integrationDeleteTasks, "Removing integrations", false);
644
- await pushStep(toolTasks, "Pushing tools", true);
645
- await pushStep(settingsTasks, "Pushing general settings", false);
646
- 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).
647
439
  // Auth fallback
648
440
  if (authFailed && authFailedTasks.length > 0 && !savedApiKey) {
649
441
  // The auth fallback prompts for an API key. In a non-TTY environment
@@ -801,12 +593,29 @@ async function _push(pushSpan, agentCode, options = {}) {
801
593
  // This ensures: if snapshot fails, user re-runs `aui push` to retry both
802
594
  // failed entity pushes AND the snapshot. Local files remain the source
803
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.
804
603
  let baselineUpdated = false;
805
604
  const canCommitBaseline = !prePushDraft || snapshotSucceeded;
806
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));
807
616
  if (failed > 0 && succeeded > 0) {
808
- if (succeededFiles.length > 0) {
809
- 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)`);
810
619
  baselineUpdated = true;
811
620
  }
812
621
  }
@@ -908,11 +717,11 @@ async function _push(pushSpan, agentCode, options = {}) {
908
717
  throw error;
909
718
  }
910
719
  }
911
- /**
912
- * Lookup the agent-management record for the current `.auirc` project (preferred)
913
- * or the active session fallbacksame precedence as draft resolution.
914
- */
915
- 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.
916
725
  const client = new AUIClient({
917
726
  baseUrl: config.apiUrl,
918
727
  authToken: config.authToken,
@@ -925,59 +734,54 @@ async function lookupAgentManagementInfoForPush(config, projectConfig, session)
925
734
  client.setAgentSettingsApiKey(key);
926
735
  let agentInfo;
927
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`).
928
740
  const projectNetworkId = projectConfig.agent_id;
929
741
  const fallbackNetworkId = session.network_id;
930
742
  if (projectNetworkId) {
931
743
  try {
932
744
  const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: projectNetworkId });
933
- agentInfo = resp.items.find((a) => a.scope.network_id === projectNetworkId ||
934
- a.id === projectNetworkId);
935
- }
936
- catch {
937
- // listing failed, fall through
745
+ agentInfo = resp.items.find((a) => a.scope.network_id === projectNetworkId || a.id === projectNetworkId);
746
+ }
747
+ catch (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
  }
757
+ // Fall back to session's agent_management_id only when not inside a project
940
758
  if (!agentInfo && !projectNetworkId && agentMgmtId) {
941
759
  try {
942
760
  agentInfo = await client.agentManagement.getAgent(agentMgmtId);
943
761
  }
944
- catch {
945
- // stale ID, fall through
762
+ catch (err) {
763
+ if (process.env.AUI_DEBUG) {
764
+ console.warn(`[debug] resolveVersionDraft: getAgent(${agentMgmtId}) failed (stale id?):`, err instanceof Error ? err.message : err);
765
+ }
946
766
  }
947
767
  }
768
+ // Last resort: session's network_id
948
769
  if (!agentInfo && fallbackNetworkId) {
949
770
  try {
950
771
  const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: fallbackNetworkId });
951
- agentInfo = resp.items.find((a) => a.scope.network_id === fallbackNetworkId ||
952
- a.id === fallbackNetworkId);
772
+ agentInfo = resp.items.find((a) => a.scope.network_id === fallbackNetworkId || a.id === fallbackNetworkId);
953
773
  }
954
- catch {
955
- // no agent-management available
774
+ catch (err) {
775
+ if (process.env.AUI_DEBUG) {
776
+ console.warn(`[debug] resolveVersionDraft: listAgents(network_id=${fallbackNetworkId}) failed:`, err instanceof Error ? err.message : err);
777
+ }
956
778
  }
957
779
  }
958
- return agentInfo;
959
- }
960
- async function resolveVersionDraft(config, projectConfig, session, explicitVersionId) {
961
- // Every error path below MUST throw a typed CLIError (not return null).
962
- // Returning null silently exits the CLI with code 0 — the BFF then thinks
963
- // the push succeeded when nothing actually happened, and the failure
964
- // never reaches Logfire because no exception bubbled to handleError.
965
- const agentInfo = await lookupAgentManagementInfoForPush(config, projectConfig, session);
966
780
  if (!agentInfo) {
967
781
  throw new ConfigError("Could not resolve agent for version management.", {
968
782
  suggestion: "Run `aui import-agent` to link an agent, or check your session with `aui status`.",
969
783
  });
970
784
  }
971
- const client = new AUIClient({
972
- baseUrl: config.apiUrl,
973
- authToken: config.authToken,
974
- accountId: config.accountId,
975
- organizationId: config.organizationId,
976
- environment: config.environment,
977
- });
978
- const key = loadAgentSettingsApiKey();
979
- if (key)
980
- client.setAgentSettingsApiKey(key);
981
785
  // If user passed --version-id, validate it's a draft
982
786
  if (explicitVersionId) {
983
787
  let ver;
@@ -1112,8 +916,14 @@ async function resolveAgentSettingsParams(config, projectConfig, session, projec
1112
916
  saveProjectConfig({ ...projectConfig, network_category_id: categoryId }, projectRoot);
1113
917
  }
1114
918
  }
1115
- catch {
1116
- // 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
+ }
1117
927
  }
1118
928
  }
1119
929
  if (!categoryId) {
@@ -1148,6 +958,215 @@ async function resolveAgentSettingsParams(config, projectConfig, session, projec
1148
958
  }
1149
959
  return baseParams;
1150
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
+ }
1151
1170
  // ─── Array File Info Helper ───
1152
1171
  function getArrayFileInfoForPush(filePath, dir) {
1153
1172
  try {
@@ -1176,11 +1195,6 @@ function getArrayFileInfoForPush(filePath, dir) {
1176
1195
  return null;
1177
1196
  }
1178
1197
  }
1179
- function isIntegrationUpsertTask(t) {
1180
- return (t.type === "put-integrations" ||
1181
- t.type === "create-integration" ||
1182
- t.type === "patch-integration");
1183
- }
1184
1198
  function writePushMemory(projectRoot, agentCode, agentId, pushTasks, succeededFiles, pushFailures) {
1185
1199
  try {
1186
1200
  const memoryDir = path.join(projectRoot, "memory");
@@ -1270,7 +1284,13 @@ function writePushMemory(projectRoot, agentCode, agentId, pushTasks, succeededFi
1270
1284
  fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
1271
1285
  return path.relative(projectRoot, filePath);
1272
1286
  }
1273
- 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
+ }
1274
1294
  return undefined;
1275
1295
  }
1276
1296
  }
@@ -1560,95 +1580,280 @@ async function executePushTask(client, params, task) {
1560
1580
  }
1561
1581
  });
1562
1582
  }
1563
- // Fall back to PATCH only on a 409 Conflict from the create call — the BFF
1564
- // returns 409 specifically for "already exists" conflicts, so any other
1565
- // status code is a real error and must be surfaced (not masked behind a
1566
- // 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.
1567
1604
  function isAlreadyExistsConflict(err) {
1568
1605
  if (!err || typeof err !== "object")
1569
1606
  return false;
1570
- 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
1615
+ ?? err.status;
1616
+ return code === 404;
1617
+ }
1618
+ function isTransient5xx(err) {
1619
+ if (!err || typeof err !== "object")
1620
+ return false;
1621
+ const code = err.statusCode
1571
1622
  ?? err.status;
1572
- return statusCode === 409;
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);
1573
1661
  }
1574
1662
  async function _executePushTask(client, params, task) {
1575
1663
  switch (task.type) {
1576
1664
  case "patch-tool":
1577
- return client.patchTool(params, task.toolName, task.body);
1578
- case "create-tool":
1579
- try {
1580
- return await client.createTool(params, task.body);
1581
- }
1582
- catch (err) {
1583
- if (isAlreadyExistsConflict(err)) {
1584
- if (process.env.AUI_DEBUG) {
1585
- 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);
1586
1675
  }
1587
- const body = task.body;
1588
- const toolCode = body.code || "";
1589
- const toolName = toolCode.toUpperCase().replace(/-/g, "_");
1590
- return client.patchTool(params, toolName, body);
1676
+ throw err;
1591
1677
  }
1592
- if (process.env.AUI_DEBUG) {
1593
- const statusCode = err.statusCode ?? err.status;
1594
- 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);
1595
1683
  }
1596
- throw err;
1597
- }
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
+ });
1598
1697
  case "delete-tool":
1599
- 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
+ });
1600
1712
  case "patch-general-settings":
1601
- return client.patchGeneralSettings(params, task.body);
1713
+ return withTransientRetry("PATCH general-settings", () => client.patchGeneralSettings(params, task.body));
1602
1714
  case "put-parameters":
1603
- return client.putParameters(params, task.body, task.oldBody);
1715
+ return withTransientRetry("PUT parameters", () => client.putParameters(params, task.body, task.oldBody));
1604
1716
  case "put-entities":
1605
- return client.putEntities(params, task.body, task.oldBody);
1717
+ return withTransientRetry("PUT entities", () => client.putEntities(params, task.body, task.oldBody));
1606
1718
  case "put-integrations":
1607
- return client.putIntegrations(params, task.body, task.oldBody);
1719
+ return withTransientRetry("PUT integrations", () => client.putIntegrations(params, task.body, task.oldBody));
1608
1720
  case "create-parameter":
1609
- try {
1610
- return await client.createParameter(params, task.body);
1611
- }
1612
- catch (err) {
1613
- if (isAlreadyExistsConflict(err)) {
1614
- 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);
1615
1724
  }
1616
- throw err;
1617
- }
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
+ });
1618
1735
  case "patch-parameter":
1619
- 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
+ });
1620
1750
  case "delete-parameter":
1621
- 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
+ });
1622
1765
  case "create-entity":
1623
- try {
1624
- return await client.createEntity(params, task.body);
1625
- }
1626
- catch (err) {
1627
- if (isAlreadyExistsConflict(err)) {
1628
- 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);
1629
1769
  }
1630
- throw err;
1631
- }
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
+ });
1632
1780
  case "patch-entity":
1633
- 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
+ });
1634
1795
  case "delete-entity":
1635
- 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
+ });
1636
1810
  case "create-integration":
1637
- try {
1638
- return await client.createIntegration(params, task.body);
1639
- }
1640
- catch (err) {
1641
- if (isAlreadyExistsConflict(err)) {
1642
- 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);
1643
1814
  }
1644
- throw err;
1645
- }
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
+ });
1646
1825
  case "patch-integration":
1647
- 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
+ });
1648
1840
  case "delete-integration":
1649
- 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
+ });
1650
1855
  case "put-rules":
1651
- return client.putRules(params, task.body);
1856
+ return withTransientRetry("PUT rules", () => client.putRules(params, task.body));
1652
1857
  default:
1653
1858
  throw new Error(`Unknown push task type: ${task.type}`);
1654
1859
  }