aui-agent-builder 0.3.103 → 0.3.105
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.
- package/dist/api-client/index.d.ts +304 -19
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/index.js +337 -69
- package/dist/api-client/index.js.map +1 -1
- package/dist/commands/agents.d.ts +55 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +193 -50
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/import-agent.d.ts +23 -0
- package/dist/commands/import-agent.d.ts.map +1 -1
- package/dist/commands/import-agent.js +886 -151
- package/dist/commands/import-agent.js.map +1 -1
- package/dist/commands/legacy/push-records-mode.d.ts +166 -0
- package/dist/commands/legacy/push-records-mode.d.ts.map +1 -0
- package/dist/commands/legacy/push-records-mode.js +2575 -0
- package/dist/commands/legacy/push-records-mode.js.map +1 -0
- package/dist/commands/pull-agent.d.ts +8 -0
- package/dist/commands/pull-agent.d.ts.map +1 -1
- package/dist/commands/pull-agent.js +598 -126
- package/dist/commands/pull-agent.js.map +1 -1
- package/dist/commands/push.d.ts +78 -107
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +1037 -1804
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/util/agent-mode.d.ts +69 -0
- package/dist/commands/util/agent-mode.d.ts.map +1 -0
- package/dist/commands/util/agent-mode.js +101 -0
- package/dist/commands/util/agent-mode.js.map +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +23 -7
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/version-snapshot.d.ts.map +1 -1
- package/dist/commands/version-snapshot.js +253 -49
- package/dist/commands/version-snapshot.js.map +1 -1
- package/dist/commands/version.d.ts +15 -1
- package/dist/commands/version.d.ts.map +1 -1
- package/dist/commands/version.js +102 -7
- package/dist/commands/version.js.map +1 -1
- package/dist/config/index.d.ts +16 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/ui/views/ImportAgentView.d.ts +15 -0
- package/dist/ui/views/ImportAgentView.d.ts.map +1 -1
- package/dist/ui/views/ImportAgentView.js +8 -3
- package/dist/ui/views/ImportAgentView.js.map +1 -1
- package/dist/utils/index.d.ts +80 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +330 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/push.js
CHANGED
|
@@ -3,20 +3,18 @@ import * as fs from "fs";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { render } from "ink";
|
|
5
5
|
import { Box, Text } from "ink";
|
|
6
|
-
import inquirer from "inquirer";
|
|
7
6
|
import { getConfig, loadProjectConfig, saveProjectConfig, loadAgentSettingsApiKey, saveAgentSettingsApiKey, } from "../config/index.js";
|
|
8
7
|
import { getValidSession } from "../services/auth.service.js";
|
|
9
|
-
import { AUIClient,
|
|
10
|
-
import { findAuiFiles, parseAuiFile } from "../utils/index.js";
|
|
11
|
-
import { validate } from "./validate.js";
|
|
8
|
+
import { AUIClient, AUIAPIError } from "../api-client/index.js";
|
|
9
|
+
import { findAuiFiles, parseAuiFile, assembleBundleFromFiles, } from "../utils/index.js";
|
|
12
10
|
import { getTracer, SpanStatusCode, setUserContext, setAgentContext, } from "../telemetry.js";
|
|
13
|
-
import {
|
|
14
|
-
import { getItemLevelDiff } from "../utils/git.js";
|
|
15
|
-
import { AuthenticationError, CLIError, ConfigError, ValidationError } from "../errors/index.js";
|
|
11
|
+
import { AuthenticationError, CLIError, ConfigError, ValidationError, } from "../errors/index.js";
|
|
16
12
|
import { StatusLine, Spinner, ErrorDisplay, Hint, } from "../ui/components/index.js";
|
|
17
13
|
import { colors, icons } from "../ui/theme.js";
|
|
18
|
-
import { PushFileSummary, PushChangesView,
|
|
14
|
+
import { PushFileSummary, PushChangesView, PushFinalSummary, } from "../ui/views/PushView.js";
|
|
19
15
|
import { isJsonMode, outputJson, stderrLog } from "../utils/json-output.js";
|
|
16
|
+
import { detectAgentBundleMode, isModeMismatchError, } from "./util/agent-mode.js";
|
|
17
|
+
import { runLegacyPush } from "./legacy/push-records-mode.js";
|
|
20
18
|
// ─── Ink Rendering Helpers ───
|
|
21
19
|
function log(node) {
|
|
22
20
|
const { unmount } = render(node);
|
|
@@ -45,9 +43,7 @@ function startSpinner(label) {
|
|
|
45
43
|
},
|
|
46
44
|
/**
|
|
47
45
|
* Internal: unconditionally unmount, no log line. Used by `withSpinner`
|
|
48
|
-
* to guarantee the spinner stops even when the wrapped body throws
|
|
49
|
-
* exception that escapes the surrounding try/catch (which would
|
|
50
|
-
* otherwise leave a phantom spinner spinning forever).
|
|
46
|
+
* to guarantee the spinner stops even when the wrapped body throws.
|
|
51
47
|
*/
|
|
52
48
|
_forceUnmount() {
|
|
53
49
|
safeUnmount();
|
|
@@ -56,12 +52,10 @@ function startSpinner(label) {
|
|
|
56
52
|
}
|
|
57
53
|
/**
|
|
58
54
|
* Wrap a sync- or async-returning callback so the spinner ALWAYS unmounts,
|
|
59
|
-
* even on uncaught exceptions.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* thrown between `startSpinner(...)` and `.succeed/.fail` leaves the Ink
|
|
64
|
-
* render mounted forever — blocking the chat UI's "Still thinking…" state.
|
|
55
|
+
* even on uncaught exceptions. Eliminates the "phantom spinner" foot-gun
|
|
56
|
+
* where an exception between `startSpinner(...)` and `.succeed/.fail` leaves
|
|
57
|
+
* the Ink render mounted forever — blocking the chat UI's "Still thinking…"
|
|
58
|
+
* state until SIGTERM.
|
|
65
59
|
*/
|
|
66
60
|
async function withSpinner(label, fn) {
|
|
67
61
|
const spinner = startSpinner(label);
|
|
@@ -73,7 +67,7 @@ async function withSpinner(label, fn) {
|
|
|
73
67
|
}
|
|
74
68
|
}
|
|
75
69
|
/**
|
|
76
|
-
* Push local agent configuration to the backend
|
|
70
|
+
* Push local agent configuration to the backend.
|
|
77
71
|
*/
|
|
78
72
|
export async function push(agentCode, options = {}) {
|
|
79
73
|
const tracer = getTracer();
|
|
@@ -96,7 +90,7 @@ export async function push(agentCode, options = {}) {
|
|
|
96
90
|
}
|
|
97
91
|
});
|
|
98
92
|
}
|
|
99
|
-
async function _push(pushSpan,
|
|
93
|
+
async function _push(pushSpan, _agentCode, options = {}) {
|
|
100
94
|
const config = getConfig();
|
|
101
95
|
const projectRoot = config.projectRoot || process.cwd();
|
|
102
96
|
if (!config.isAuthenticated) {
|
|
@@ -120,201 +114,71 @@ async function _push(pushSpan, agentCode, options = {}) {
|
|
|
120
114
|
});
|
|
121
115
|
}
|
|
122
116
|
const json = isJsonMode();
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
117
|
+
// ─── Back-compat: --force / --skip-validation are no-ops ───
|
|
118
|
+
// The new push endpoint runs the v1 transcoder bundle-validate as the
|
|
119
|
+
// first thing it does (before the per-version Redis lock and any GCS
|
|
120
|
+
// write). We keep the flags so existing scripts still parse, and
|
|
121
|
+
// record on the span whether they were requested for traceability.
|
|
128
122
|
pushSpan.setAttribute("push.force_used", options.force === true);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Wrap the validate call in its own span so a "stuck at validate"
|
|
135
|
-
// hang shows up clearly in Logfire as `aui.push.preflight.validate`
|
|
136
|
-
// with status = unset (still running) — instead of the parent
|
|
137
|
-
// `aui.push` span just sitting there with no clue why.
|
|
138
|
-
const validateTracer = getTracer();
|
|
139
|
-
// Captured via the `onResult` callback below. Held in a ref so it's
|
|
140
|
-
// available outside the closure where the bypass event fires. The
|
|
141
|
-
// explicit `ValidateResultSummary | null` type is necessary —
|
|
142
|
-
// TypeScript's strict control-flow analysis would otherwise narrow
|
|
143
|
-
// the declared type to `null` after the await, since the assignment
|
|
144
|
-
// happens inside a callback whose execution it can't trace.
|
|
145
|
-
let validateSummary = null;
|
|
146
|
-
const valid = await validateTracer.startActiveSpan("aui.push.preflight.validate", async (vSpan) => {
|
|
147
|
-
vSpan.setAttribute("push.preflight.step", "validate");
|
|
148
|
-
vSpan.setAttribute("push.preflight.skipValidation", false);
|
|
149
|
-
vSpan.setAttribute("push.preflight.force", options.force === true);
|
|
150
|
-
// Validation now hits the agent-settings backend (see
|
|
151
|
-
// commands/validate.tsx). Surface the agent identifiers on this
|
|
152
|
-
// span so a Logfire row tells the on-call exactly which agent
|
|
153
|
-
// was being validated when the preflight failed — without
|
|
154
|
-
// having to drill into the nested `aui.validate.remote` child.
|
|
155
|
-
await setUserContext(vSpan);
|
|
156
|
-
await setAgentContext(vSpan, {
|
|
157
|
-
agentId: projectConfig.agent_id,
|
|
158
|
-
agentCode: projectConfig.agent_code,
|
|
159
|
-
agentManagementId: projectConfig.agent_management_id,
|
|
160
|
-
versionId: projectConfig.version_id,
|
|
161
|
-
versionLabel: projectConfig.version_label,
|
|
162
|
-
networkId: projectConfig.agent_id,
|
|
163
|
-
accountId: projectConfig.account_id,
|
|
164
|
-
organizationId: projectConfig.organization_id,
|
|
165
|
-
networkCategoryId: projectConfig.network_category_id,
|
|
166
|
-
});
|
|
167
|
-
try {
|
|
168
|
-
const ok = await validate(projectRoot, {
|
|
169
|
-
verbose: false,
|
|
170
|
-
onResult: (s) => {
|
|
171
|
-
validateSummary = s;
|
|
172
|
-
// Denormalize the validate result onto the preflight span
|
|
173
|
-
// so the trace's top-level shows the verdict + counts
|
|
174
|
-
// without having to expand the nested `aui.validate`
|
|
175
|
-
// child. Especially valuable when --force later
|
|
176
|
-
// bypasses: the bypass event reads from these.
|
|
177
|
-
vSpan.setAttribute("push.preflight.validate.ok", s.ok);
|
|
178
|
-
vSpan.setAttribute("push.preflight.validate.error_count", s.errorCount);
|
|
179
|
-
vSpan.setAttribute("push.preflight.validate.warning_count", s.warningCount);
|
|
180
|
-
vSpan.setAttribute("push.preflight.validate.remote_reachable", s.remoteReachable);
|
|
181
|
-
if (s.remoteValid !== null) {
|
|
182
|
-
vSpan.setAttribute("push.preflight.validate.remote_valid", s.remoteValid);
|
|
183
|
-
}
|
|
184
|
-
if (s.remoteStatusCode !== null) {
|
|
185
|
-
vSpan.setAttribute("push.preflight.validate.remote_status_code", s.remoteStatusCode);
|
|
186
|
-
}
|
|
187
|
-
if (s.remoteFailureReason) {
|
|
188
|
-
vSpan.setAttribute("push.preflight.validate.remote_failure_reason", s.remoteFailureReason);
|
|
189
|
-
}
|
|
190
|
-
if (s.issueCodes.length > 0) {
|
|
191
|
-
vSpan.setAttribute("push.preflight.validate.issue_codes", s.issueCodes.join(","));
|
|
192
|
-
}
|
|
193
|
-
if (s.topIssueMessages.length > 0) {
|
|
194
|
-
vSpan.setAttribute("push.preflight.validate.top_messages", JSON.stringify(s.topIssueMessages));
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
vSpan.setAttribute("push.preflight.validate.ok", ok);
|
|
199
|
-
vSpan.setStatus({ code: SpanStatusCode.OK });
|
|
200
|
-
return ok;
|
|
201
|
-
}
|
|
202
|
-
catch (err) {
|
|
203
|
-
// validate() shouldn't throw under normal conditions, but if a
|
|
204
|
-
// schema fetch or git call inside it does, surface it here so
|
|
205
|
-
// we don't lose the error to the parent span's generic handler.
|
|
206
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
207
|
-
vSpan.setStatus({ code: SpanStatusCode.ERROR, message: msg });
|
|
208
|
-
vSpan.recordException(err instanceof Error ? err : new Error(msg));
|
|
209
|
-
throw err;
|
|
210
|
-
}
|
|
211
|
-
finally {
|
|
212
|
-
vSpan.end();
|
|
213
|
-
}
|
|
123
|
+
pushSpan.setAttribute("push.skip_validation_flag_used", options.skipValidation === true);
|
|
124
|
+
if (options.force || options.skipValidation) {
|
|
125
|
+
pushSpan.addEvent("preflight.legacy_flag_noop", {
|
|
126
|
+
force: options.force === true,
|
|
127
|
+
skip_validation: options.skipValidation === true,
|
|
214
128
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// The remote validator may return false because:
|
|
218
|
-
// • the backend reported `valid: false` (real validation errors)
|
|
219
|
-
// • the backend was unreachable / 5xx (infra failure)
|
|
220
|
-
// • the local project lacks an agent_id or auth (cannot run validate)
|
|
221
|
-
// In every case we refuse to push so the user can fix the underlying
|
|
222
|
-
// issue. `--force` remains the explicit escape hatch for emergencies
|
|
223
|
-
// (matches the pre-remote-validate contract documented in this file).
|
|
224
|
-
if (!valid && !options.force) {
|
|
225
|
-
pushSpan.setAttribute("push.exit_reason", "validation_failed");
|
|
226
|
-
pushSpan.addEvent("preflight.validation_rejected_push");
|
|
227
|
-
throw new ValidationError("Push aborted due to validation errors.", {
|
|
228
|
-
suggestion: "Fix the errors above, or use --force to push anyway.",
|
|
229
|
-
});
|
|
129
|
+
if (!json) {
|
|
130
|
+
log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "muted", label: `${options.force ? "--force" : "--skip-validation"} is a no-op under the new push endpoint (server validates the bundle).` }) }));
|
|
230
131
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// attached to the preflight span (error count, codes, top
|
|
234
|
-
// messages, remote reachability) so anyone reading the trace —
|
|
235
|
-
// or filtering Logfire for `event.name = 'preflight.validation_failed_but_forced'`
|
|
236
|
-
// — sees WHY validation failed and what was bypassed without
|
|
237
|
-
// needing to JOIN against nested spans. This is the closest we
|
|
238
|
-
// get to an audit log of "who pushed broken stuff and what was
|
|
239
|
-
// broken about it" without changing user behaviour.
|
|
240
|
-
//
|
|
241
|
-
// Cast: TS narrows `validateSummary` to `null` after the await
|
|
242
|
-
// because it can't see the callback set it (control-flow analysis
|
|
243
|
-
// doesn't track mutation through closures). Cast back to the
|
|
244
|
-
// declared union — the runtime value really is `ValidateResultSummary | null`.
|
|
245
|
-
const s = validateSummary;
|
|
246
|
-
pushSpan.setAttribute("push.preflight.validate.bypassed", true);
|
|
247
|
-
if (s) {
|
|
248
|
-
pushSpan.setAttribute("push.preflight.validate.bypassed.error_count", s.errorCount);
|
|
249
|
-
pushSpan.setAttribute("push.preflight.validate.bypassed.warning_count", s.warningCount);
|
|
250
|
-
pushSpan.setAttribute("push.preflight.validate.bypassed.remote_reachable", s.remoteReachable);
|
|
251
|
-
if (s.issueCodes.length > 0) {
|
|
252
|
-
pushSpan.setAttribute("push.preflight.validate.bypassed.issue_codes", s.issueCodes.join(","));
|
|
253
|
-
}
|
|
254
|
-
if (s.topIssueMessages.length > 0) {
|
|
255
|
-
pushSpan.setAttribute("push.preflight.validate.bypassed.top_messages", JSON.stringify(s.topIssueMessages));
|
|
256
|
-
}
|
|
257
|
-
if (s.remoteFailureReason) {
|
|
258
|
-
pushSpan.setAttribute("push.preflight.validate.bypassed.remote_failure_reason", s.remoteFailureReason);
|
|
259
|
-
}
|
|
260
|
-
if (s.remoteStatusCode !== null) {
|
|
261
|
-
pushSpan.setAttribute("push.preflight.validate.bypassed.remote_status_code", s.remoteStatusCode);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
pushSpan.addEvent("preflight.validation_failed_but_forced", {
|
|
265
|
-
error_count: s?.errorCount ?? -1,
|
|
266
|
-
warning_count: s?.warningCount ?? -1,
|
|
267
|
-
remote_reachable: s?.remoteReachable ?? false,
|
|
268
|
-
issue_codes: s?.issueCodes.join(",") ?? "",
|
|
269
|
-
top_messages: s ? JSON.stringify(s.topIssueMessages) : "",
|
|
270
|
-
remote_failure_reason: s?.remoteFailureReason ?? "",
|
|
271
|
-
});
|
|
272
|
-
// Also yell at the user on stderr so they can't miss it in their
|
|
273
|
-
// terminal. JSON mode: stderr too — the user/agent reading JSON
|
|
274
|
-
// output still sees stderr separately from the JSON envelope.
|
|
275
|
-
const summary_line = s
|
|
276
|
-
? `--force used: bypassing ${s.errorCount} error(s) and ${s.warningCount} warning(s) from validate.` +
|
|
277
|
-
(s.issueCodes.length
|
|
278
|
-
? ` Codes: ${s.issueCodes.join(", ")}.`
|
|
279
|
-
: "") +
|
|
280
|
-
(s.remoteReachable
|
|
281
|
-
? ""
|
|
282
|
-
: ` Validate endpoint was unreachable (${s.remoteFailureReason ?? "unknown"}).`)
|
|
283
|
-
: "--force used: bypassing validation failure.";
|
|
284
|
-
stderrLog(`⚠️ ${summary_line}`);
|
|
132
|
+
else {
|
|
133
|
+
stderrLog(`${options.force ? "--force" : "--skip-validation"} is a no-op under the new push endpoint.`);
|
|
285
134
|
}
|
|
286
135
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
136
|
+
// ─── Read every .aui.json under the project ───
|
|
137
|
+
// The new push endpoint expects a FULL bundle — we no longer split
|
|
138
|
+
// changes from baseline files. Read everything once up-front.
|
|
290
139
|
if (!json)
|
|
291
|
-
log(_jsx(StatusLine, { kind: "info", label: "
|
|
140
|
+
log(_jsx(StatusLine, { kind: "info", label: "Reading local agent files..." }));
|
|
292
141
|
else
|
|
293
142
|
stderrLog("Reading local files...");
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
try {
|
|
143
|
+
const fileData = await withSpinner("Reading local files...", async (spinner) => {
|
|
144
|
+
const out = [];
|
|
297
145
|
const files = await findAuiFiles(projectRoot);
|
|
298
|
-
for (const
|
|
299
|
-
const parsed = parseAuiFile(
|
|
146
|
+
for (const f of files) {
|
|
147
|
+
const parsed = parseAuiFile(f);
|
|
300
148
|
if (!parsed)
|
|
301
149
|
continue;
|
|
302
|
-
const relativePath = path.relative(projectRoot,
|
|
303
|
-
let type = "unknown";
|
|
150
|
+
const relativePath = path.relative(projectRoot, f);
|
|
304
151
|
const isToolPath = relativePath.startsWith("tools/") || relativePath.startsWith("tools\\");
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// the
|
|
313
|
-
|
|
314
|
-
|
|
152
|
+
// Classify by filename first (the new /pull endpoint enforces
|
|
153
|
+
// canonical, stable filenames at the bundle root), then fall
|
|
154
|
+
// back to content-based detection for non-canonical files.
|
|
155
|
+
//
|
|
156
|
+
// Why filename-first: `agent.aui.json` historically was written
|
|
157
|
+
// wrapped — `{ "general_settings": {...} }` — by the legacy
|
|
158
|
+
// exportAgent path, so `parsed.general_settings` was a reliable
|
|
159
|
+
// probe. The new /pull endpoint returns the file body the
|
|
160
|
+
// server stored, and per the spec the canonical shape for
|
|
161
|
+
// `agent.aui.json` is the UNWRAPPED general_settings object
|
|
162
|
+
// (`{ name, objective, guardrails, ... }` directly). The
|
|
163
|
+
// wrapped-only check therefore mis-classified every freshly
|
|
164
|
+
// pulled `agent.aui.json` as "unknown" — purely a label bug
|
|
165
|
+
// (the file still uploaded fine) but confusing in the summary.
|
|
166
|
+
const basename = path.basename(relativePath);
|
|
167
|
+
let type = "unknown";
|
|
168
|
+
if (basename === "agent.aui.json")
|
|
315
169
|
type = "general_settings";
|
|
170
|
+
else if (basename === "parameters.aui.json")
|
|
171
|
+
type = "parameters";
|
|
172
|
+
else if (basename === "entities.aui.json")
|
|
173
|
+
type = "entities";
|
|
174
|
+
else if (basename === "integrations.aui.json")
|
|
175
|
+
type = "integrations";
|
|
176
|
+
else if (basename === "rules.aui.json")
|
|
177
|
+
type = "rules";
|
|
316
178
|
else if (parsed.tool || isToolPath)
|
|
317
179
|
type = "tool";
|
|
180
|
+
else if (parsed.general_settings)
|
|
181
|
+
type = "general_settings"; // legacy wrapped
|
|
318
182
|
else if (parsed.parameters)
|
|
319
183
|
type = "parameters";
|
|
320
184
|
else if (parsed.entities)
|
|
@@ -323,750 +187,746 @@ async function _push(pushSpan, agentCode, options = {}) {
|
|
|
323
187
|
type = "integrations";
|
|
324
188
|
else if (parsed.rules)
|
|
325
189
|
type = "rules";
|
|
326
|
-
|
|
190
|
+
out.push({ file: relativePath, type, data: parsed });
|
|
327
191
|
}
|
|
192
|
+
spinner.succeed(`Read ${out.length} files`);
|
|
328
193
|
if (json)
|
|
329
|
-
stderrLog(`Read ${
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
194
|
+
stderrLog(`Read ${out.length} files`);
|
|
195
|
+
return out;
|
|
196
|
+
});
|
|
197
|
+
if (fileData.length === 0) {
|
|
198
|
+
pushSpan.setAttribute("push.exit_reason", "no_files");
|
|
199
|
+
throw new CLIError("No .aui.json files found in this project to push.", {
|
|
200
|
+
suggestion: "Run `aui import` (or `aui pull`) to download the current agent bundle first.",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// The previous multipart endpoint required path-separator-free,
|
|
204
|
+
// unique filenames per request. The new JSON endpoint sends ONE
|
|
205
|
+
// canonical `bundle.json` — there are no filenames on the wire
|
|
206
|
+
// anymore, so the old collision check is gone. Tool files still
|
|
207
|
+
// need locally-unique basenames to avoid one .aui.json overwriting
|
|
208
|
+
// another in the project tree, but that's a project-layout issue,
|
|
209
|
+
// not a push-time one.
|
|
210
|
+
const summary = {};
|
|
211
|
+
for (const fd of fileData)
|
|
212
|
+
summary[fd.type] = (summary[fd.type] || 0) + 1;
|
|
213
|
+
const pushFiles = fileData.map((fd) => ({
|
|
214
|
+
file: fd.file,
|
|
215
|
+
type: fd.type,
|
|
216
|
+
}));
|
|
217
|
+
if (!json) {
|
|
218
|
+
log(_jsx(PushFileSummary, { agentCode: projectConfig.agent_code || projectConfig.agent_id, agentId: projectConfig.agent_id, files: pushFiles, summary: summary }));
|
|
219
|
+
}
|
|
220
|
+
// ─── Git-based diff (informational + baseline tracking) ───
|
|
221
|
+
// The new endpoint uploads the full bundle every time so the diff is
|
|
222
|
+
// *not* used to decide what to send. We still compute it for:
|
|
223
|
+
// - showing the user what they changed (UX)
|
|
224
|
+
// - dry-run preview
|
|
225
|
+
// - deciding whether to commit a baseline at the end
|
|
226
|
+
const { getDiffSummary, getFileDiff, getItemLevelDiff: getItemDiff, hasOwnGitRepo, initAndCommitBaseline, commitBaseline, } = await import("../utils/git.js");
|
|
227
|
+
if (!hasOwnGitRepo(projectRoot)) {
|
|
228
|
+
pushSpan.setAttribute("push.exit_reason", "baseline_initialized");
|
|
229
|
+
initAndCommitBaseline(projectRoot, "baseline: initialized from existing files");
|
|
230
|
+
if (json) {
|
|
231
|
+
outputJson({
|
|
232
|
+
dry_run: options.dryRun ?? false,
|
|
233
|
+
baseline_initialized: true,
|
|
234
|
+
has_changes: false,
|
|
235
|
+
});
|
|
365
236
|
return;
|
|
366
237
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
238
|
+
log(_jsx(Box, { paddingX: 1, children: _jsx(Hint, { message: "Baseline initialized. Run `aui push` again to upload the current bundle." }) }));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const diff = getDiffSummary(projectRoot);
|
|
242
|
+
if (diff && !json) {
|
|
243
|
+
const changeFiles = diff.changedFiles.map((change) => {
|
|
244
|
+
const result = {
|
|
245
|
+
file: change.file,
|
|
246
|
+
status: change.status,
|
|
247
|
+
};
|
|
248
|
+
if ((change.status === "modified" || change.status === "added") &&
|
|
249
|
+
!change.file.startsWith("knowledge-hubs/")) {
|
|
250
|
+
const arrayFileInfo = getArrayFileInfoForPush(change.file, projectRoot);
|
|
251
|
+
if (arrayFileInfo) {
|
|
252
|
+
const itemChanges = getItemDiff(projectRoot, change.file, arrayFileInfo.arrayKey, arrayFileInfo.itemKey);
|
|
253
|
+
if (itemChanges.length > 0) {
|
|
254
|
+
result.itemChanges = itemChanges.map((item) => ({
|
|
255
|
+
key: item.key,
|
|
256
|
+
status: item.status,
|
|
257
|
+
fieldChanges: item.fieldChanges,
|
|
258
|
+
}));
|
|
385
259
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
260
|
+
}
|
|
261
|
+
else if (change.status === "modified") {
|
|
262
|
+
const fieldDiffs = getFileDiff(projectRoot, change.file);
|
|
263
|
+
if (fieldDiffs.length > 0) {
|
|
264
|
+
result.fieldDiffs = fieldDiffs;
|
|
391
265
|
}
|
|
392
266
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
});
|
|
270
|
+
log(_jsx(PushChangesView, { changes: changeFiles }));
|
|
271
|
+
}
|
|
272
|
+
// ─── Dry run: don't contact the backend at all ───
|
|
273
|
+
if (options.dryRun) {
|
|
274
|
+
pushSpan.setAttribute("push.exit_reason", "dry_run");
|
|
275
|
+
if (json) {
|
|
276
|
+
outputJson({
|
|
277
|
+
dry_run: true,
|
|
278
|
+
agent: {
|
|
279
|
+
code: projectConfig.agent_code || projectConfig.agent_id,
|
|
280
|
+
id: projectConfig.agent_id,
|
|
281
|
+
},
|
|
282
|
+
files: pushFiles,
|
|
283
|
+
summary,
|
|
284
|
+
changes: diff
|
|
285
|
+
? diff.changedFiles.map((change) => {
|
|
286
|
+
const result = {
|
|
287
|
+
file: change.file,
|
|
288
|
+
status: change.status,
|
|
289
|
+
};
|
|
290
|
+
if (change.status === "modified" || change.status === "added") {
|
|
291
|
+
const arrayFileInfo = getArrayFileInfoForPush(change.file, projectRoot);
|
|
292
|
+
if (arrayFileInfo) {
|
|
293
|
+
const itemChanges = getItemDiff(projectRoot, change.file, arrayFileInfo.arrayKey, arrayFileInfo.itemKey);
|
|
294
|
+
if (itemChanges.length > 0) {
|
|
295
|
+
result.item_changes = itemChanges;
|
|
421
296
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
297
|
+
}
|
|
298
|
+
else if (change.status === "modified") {
|
|
299
|
+
const fieldDiffs = getFileDiff(projectRoot, change.file);
|
|
300
|
+
if (fieldDiffs.length > 0) {
|
|
301
|
+
result.field_diffs = fieldDiffs;
|
|
427
302
|
}
|
|
428
303
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "muted", label: "Dry run \u2014 no changes pushed." }) }));
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
// ─── Agent Config Push ───
|
|
439
|
-
//
|
|
440
|
-
// Knowledge Bases used to be pushed here (BEFORE entity writes) with a
|
|
441
|
-
// special pre-step that pushed integrations even earlier so KB uploads
|
|
442
|
-
// would find their integration. That ordering caused two production
|
|
443
|
-
// bugs: integrations were PATCHed before the parameters they reference
|
|
444
|
-
// existed (CTS-12425), and tools were pushed in parallel with their
|
|
445
|
-
// dependencies (CTS-12426). The KB push has been moved into the unified
|
|
446
|
-
// dependency-ordered flow below — see `pushKnowledgeHubs` invocation.
|
|
447
|
-
if (!diff || !diff.hasChanges) {
|
|
448
|
-
pushSpan.setAttribute("push.exit_reason", "no_agent_config_changes");
|
|
449
|
-
log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: "No agent config changes to push." }) }));
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
})
|
|
307
|
+
: [],
|
|
308
|
+
});
|
|
450
309
|
return;
|
|
451
310
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
311
|
+
log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "muted", label: `Dry run — would push ${fileData.length} file(s) as a single bundle. Nothing uploaded.` }) }));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// CHERRY-PICK NOTE (53a6024 + d7d91ab — 2026-05-24)
|
|
315
|
+
// ------------------------------------------------------------------
|
|
316
|
+
// The incoming hunk added a `hasKbChanges` short-circuit + per-entity
|
|
317
|
+
// `buildPushTasks` plumbing to the records-mode push pipeline. In our
|
|
318
|
+
// dual-mode CLI that pipeline lives in
|
|
319
|
+
// `commands/legacy/push-records-mode.tsx`, NOT here — `push.tsx`
|
|
320
|
+
// assembles a single bundle and POSTs it via `/push`.
|
|
321
|
+
//
|
|
322
|
+
// For BUNDLE-MODE pushes, "KB-only changes" can't be silently
|
|
323
|
+
// dropped: the assembled bundle is sent regardless (the server
|
|
324
|
+
// diff-compares against the previous revision), and the KB push
|
|
325
|
+
// is its own independent step via `pushKnowledgeHubs` below. The
|
|
326
|
+
// "no pushable changes" short-circuit in bundle-mode is already
|
|
327
|
+
// handled by the `diff.hasChanges` check higher up.
|
|
328
|
+
//
|
|
329
|
+
// The EQUIVALENT `hasKbChanges` fix for RECORDS-MODE pushes is
|
|
330
|
+
// applied directly inside `runLegacyPush` — see
|
|
331
|
+
// commands/legacy/push-records-mode.tsx where the legacy
|
|
332
|
+
// `pushTasks` short-circuit lives. Keep both in sync if the
|
|
333
|
+
// hasKbChanges semantics ever change again.
|
|
334
|
+
// ─── Resolve a draft version to push to ───
|
|
335
|
+
// The new endpoint enforces `status === draft`; resolve up-front so we
|
|
336
|
+
// surface a clean error instead of a 422 round-trip.
|
|
337
|
+
const session = await getValidSession();
|
|
338
|
+
if (!session) {
|
|
339
|
+
throw new AuthenticationError("Session expired.");
|
|
340
|
+
}
|
|
341
|
+
// `--agent-id` overlay: if the user passed an explicit target agent,
|
|
342
|
+
// dual-shape resolve it (agent_management_id first, fall back to
|
|
343
|
+
// network_id on 404) and overlay the resolved IDs onto `projectConfig`.
|
|
344
|
+
// Downstream (`resolveVersionDraft` → `lookupAgentManagementInfoForPush`)
|
|
345
|
+
// then picks up the override via the same code path as a normal
|
|
346
|
+
// `.auirc`-driven push — no special-casing.
|
|
347
|
+
let resolveProjectConfig = projectConfig;
|
|
348
|
+
if (options.agentId) {
|
|
349
|
+
const overlayClient = new AUIClient({
|
|
464
350
|
baseUrl: config.apiUrl,
|
|
465
351
|
authToken: config.authToken,
|
|
466
352
|
accountId: config.accountId,
|
|
467
353
|
organizationId: config.organizationId,
|
|
468
354
|
environment: config.environment,
|
|
469
355
|
});
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
356
|
+
const overlayKey = loadAgentSettingsApiKey();
|
|
357
|
+
if (overlayKey)
|
|
358
|
+
overlayClient.setAgentSettingsApiKey(overlayKey);
|
|
359
|
+
const resolved = await resolveExplicitAgentForPush(overlayClient, options.agentId);
|
|
360
|
+
resolveProjectConfig = {
|
|
361
|
+
...projectConfig,
|
|
362
|
+
agent_id: resolved.networkId ?? projectConfig.agent_id,
|
|
363
|
+
agent_management_id: resolved.agentManagementId,
|
|
364
|
+
};
|
|
365
|
+
pushSpan.setAttribute("push.agent_id_override", options.agentId);
|
|
366
|
+
pushSpan.setAttribute("push.agent_id_override_resolved_as", resolved.via);
|
|
367
|
+
pushSpan.setAttribute("push.agent_management_id_overlay", resolved.agentManagementId);
|
|
368
|
+
}
|
|
369
|
+
// ─── Pre-flight: resolve agent + detect mode BEFORE draft lookup ──
|
|
370
|
+
//
|
|
371
|
+
// We do BOTH the agent-management lookup AND the bundle_mode
|
|
372
|
+
// dispatch up-front, before kicking off `resolveVersionDraft`.
|
|
373
|
+
// This catches two cases that otherwise crash later in the flow:
|
|
374
|
+
//
|
|
375
|
+
// A) Pure-legacy agent (no agent_management_id anywhere)
|
|
376
|
+
// → `lookupAgentManagementInfoForPush` returns no agentInfo.
|
|
377
|
+
// We route directly to `runLegacyPush`, which uses `.auirc`
|
|
378
|
+
// scope filters (network_id + account_id + organization_id
|
|
379
|
+
// + network_category_id) for the per-entity write calls and
|
|
380
|
+
// doesn't need an agent_management_id or version_id at all.
|
|
381
|
+
// Sibling of the same fix in `commands/import-agent.tsx:doImport`.
|
|
382
|
+
//
|
|
383
|
+
// B) Records-mode agent without draft versions
|
|
384
|
+
// → `lookupAgentManagementInfoForPush` succeeds, but the agent
|
|
385
|
+
// is records-mode (`bundle_mode: false`) AND has no draft
|
|
386
|
+
// versions. Running `resolveVersionDraft` would throw
|
|
387
|
+
// "No draft version found" even though `runLegacyPush` can
|
|
388
|
+
// push the same agent without any version at all (legacy
|
|
389
|
+
// records-mode never required versions). Repro 2026-05-26:
|
|
390
|
+
// `Jaber-Amazon-v3-8` — has agent_management_id, has zero
|
|
391
|
+
// versions, was failing to push.
|
|
392
|
+
//
|
|
393
|
+
// For bundle-mode agents we continue to require draft-version
|
|
394
|
+
// resolution (the new `/push` endpoint is keyed on version_id),
|
|
395
|
+
// which fires as the existing flow below.
|
|
396
|
+
//
|
|
397
|
+
// Performance: this folds two upstream lookups (the one I was
|
|
398
|
+
// already doing pre-flight + the one resolveVersionDraft would do
|
|
399
|
+
// internally) into ONE — net -1 round-trip on bundle-mode pushes
|
|
400
|
+
// versus the previous version of this file.
|
|
401
|
+
const preflightLookup = await lookupAgentManagementInfoForPush(config, resolveProjectConfig, session);
|
|
402
|
+
// (A) Pure-legacy short-circuit — no agentInfo means no
|
|
403
|
+
// agent-management entry → records-mode by definition.
|
|
404
|
+
if (!preflightLookup.agentInfo) {
|
|
405
|
+
pushSpan.addEvent("dispatch.records_mode_handoff_pure_legacy", {
|
|
406
|
+
reason: "no_agent_management_id",
|
|
407
|
+
network_id: resolveProjectConfig.agent_id ?? "(unknown)",
|
|
408
|
+
errors: preflightLookup.errors.join("; ") || "(no errors)",
|
|
409
|
+
});
|
|
410
|
+
pushSpan.setAttribute("push.dispatch.mode", "records");
|
|
411
|
+
pushSpan.setAttribute("push.dispatch.mode_source", "no_agent_management_id");
|
|
412
|
+
if (process.env.AUI_DEBUG) {
|
|
413
|
+
console.log(`[debug] push: no agent_management_id resolvable for network ${resolveProjectConfig.agent_id ?? "(unknown)"} — routing to runLegacyPush (pure-legacy agent)`);
|
|
414
|
+
}
|
|
415
|
+
return runLegacyPush(pushSpan, _agentCode, {
|
|
416
|
+
force: options.force,
|
|
417
|
+
skipValidation: options.skipValidation,
|
|
418
|
+
dryRun: options.dryRun,
|
|
419
|
+
apiKey: options.apiKey,
|
|
420
|
+
versionId: options.versionId,
|
|
421
|
+
scopeLevel: options.scopeLevel,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// (B) Records-mode short-circuit — agentInfo exists, but the
|
|
425
|
+
// agent is bundle_mode=false. Records-mode push doesn't need
|
|
426
|
+
// a draft version (legacy per-entity writes operate on scope
|
|
427
|
+
// filters + agent_id alone). Route to `runLegacyPush` BEFORE
|
|
428
|
+
// `resolveVersionDraft` would throw "No draft version found"
|
|
429
|
+
// for agents that legitimately have no drafts.
|
|
430
|
+
//
|
|
431
|
+
// Uses the cached AgentInfo from the pre-flight, so
|
|
432
|
+
// `detectAgentBundleMode` doesn't make an extra round-trip.
|
|
433
|
+
// The `options.versionId` escape hatch still routes through
|
|
434
|
+
// the draft resolution below — if the user EXPLICITLY passed
|
|
435
|
+
// a version-id we honor it, even for records-mode (lets
|
|
436
|
+
// advanced users target a specific records-mode version).
|
|
437
|
+
const preflightModeResolution = await detectAgentBundleMode(new AUIClient({
|
|
438
|
+
baseUrl: config.apiUrl,
|
|
439
|
+
authToken: config.authToken,
|
|
440
|
+
accountId: config.accountId,
|
|
441
|
+
organizationId: config.organizationId,
|
|
442
|
+
environment: config.environment,
|
|
443
|
+
}), preflightLookup.agentInfo.id, preflightLookup.agentInfo);
|
|
444
|
+
if (preflightModeResolution.mode === "records" && !options.versionId) {
|
|
445
|
+
pushSpan.addEvent("dispatch.records_mode_handoff_no_draft_required", {
|
|
446
|
+
agent_management_id: preflightLookup.agentInfo.id,
|
|
447
|
+
bundle_mode: preflightLookup.agentInfo.bundle_mode ?? "(missing)",
|
|
448
|
+
source: preflightModeResolution.source,
|
|
449
|
+
});
|
|
450
|
+
pushSpan.setAttribute("push.dispatch.mode", "records");
|
|
451
|
+
pushSpan.setAttribute("push.dispatch.mode_source", preflightModeResolution.source);
|
|
452
|
+
if (process.env.AUI_DEBUG) {
|
|
453
|
+
console.log(`[debug] push: records-mode agent ${preflightLookup.agentInfo.id} — routing to runLegacyPush directly (skipping draft resolution)`);
|
|
454
|
+
}
|
|
455
|
+
return runLegacyPush(pushSpan, _agentCode, {
|
|
456
|
+
force: options.force,
|
|
457
|
+
skipValidation: options.skipValidation,
|
|
458
|
+
dryRun: options.dryRun,
|
|
459
|
+
apiKey: options.apiKey,
|
|
460
|
+
versionId: options.versionId,
|
|
461
|
+
scopeLevel: options.scopeLevel,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const resolveTracer = getTracer();
|
|
465
|
+
const prePushDraft = await resolveTracer.startActiveSpan("aui.push.preflight.resolve-version", async (rSpan) => {
|
|
466
|
+
rSpan.setAttribute("push.preflight.step", "resolve-version");
|
|
467
|
+
rSpan.setAttribute("push.preflight.has_explicit_version_id", !!options.versionId);
|
|
468
|
+
rSpan.setAttribute("push.preflight.has_explicit_agent_id", !!options.agentId);
|
|
469
|
+
rSpan.setAttribute("push.preflight.has_auirc_version_id", !!projectConfig.version_id);
|
|
470
|
+
if (resolveProjectConfig.agent_id) {
|
|
471
|
+
rSpan.setAttribute("push.preflight.network_id", resolveProjectConfig.agent_id);
|
|
557
472
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
473
|
+
try {
|
|
474
|
+
const draft = await resolveVersionDraft(config, resolveProjectConfig, session, options.versionId);
|
|
475
|
+
rSpan.setAttribute("push.preflight.resolved_version_id", draft.versionId);
|
|
476
|
+
rSpan.setAttribute("push.preflight.resolved_version_label", draft.label);
|
|
477
|
+
rSpan.setAttribute("push.preflight.resolved_agent_id", draft.agentId);
|
|
478
|
+
rSpan.setStatus({ code: SpanStatusCode.OK });
|
|
479
|
+
return draft;
|
|
564
480
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
pushSpan.setAttribute("push.exit_reason", "no_pushable_tasks");
|
|
571
|
-
log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "success", label: "No pushable changes detected." }) }));
|
|
572
|
-
return;
|
|
481
|
+
catch (err) {
|
|
482
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
483
|
+
rSpan.setStatus({ code: SpanStatusCode.ERROR, message: msg });
|
|
484
|
+
rSpan.recordException(err instanceof Error ? err : new Error(msg));
|
|
485
|
+
throw err;
|
|
573
486
|
}
|
|
574
|
-
|
|
575
|
-
|
|
487
|
+
finally {
|
|
488
|
+
rSpan.end();
|
|
576
489
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
code === 403 ||
|
|
590
|
-
msg.includes("unauthorized") ||
|
|
591
|
-
msg.includes("forbidden") ||
|
|
592
|
-
msg.includes("not a member") ||
|
|
593
|
-
msg.includes("not member"));
|
|
594
|
-
};
|
|
595
|
-
const agentCodeStr = projectConfig.agent_code || projectConfig.agent_id || "unknown";
|
|
596
|
-
const agentIdStr = projectConfig.agent_id || "unknown";
|
|
597
|
-
/**
|
|
598
|
-
* Run one push step (a group of related tasks for one entity-type)
|
|
599
|
-
* STRICTLY SEQUENTIALLY. There is intentionally no `parallel` flag — the
|
|
600
|
-
* agent-settings backend has no optimistic locking and concurrent writes
|
|
601
|
-
* to the same agent silently merge / drop unresolvable references / re-
|
|
602
|
-
* sequence array items (see file header doc + CTS-12340 / -12425 / -12426
|
|
603
|
-
* for prior incidents). If you think you need to parallelize, you don't.
|
|
604
|
-
*/
|
|
605
|
-
const pushStep = async (tasks, label) => {
|
|
606
|
-
if (tasks.length === 0)
|
|
607
|
-
return true;
|
|
608
|
-
log(_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.info, children: [icons.bullet, " ", label, "..."] }) }));
|
|
609
|
-
const stepFailed = [];
|
|
610
|
-
try {
|
|
611
|
-
for (const task of tasks) {
|
|
612
|
-
const taskResult = {
|
|
613
|
-
label: task.label,
|
|
614
|
-
status: "success",
|
|
615
|
-
};
|
|
616
|
-
try {
|
|
617
|
-
const result = await executePushTask(client, agentSettingsParams, task);
|
|
618
|
-
succeeded++;
|
|
619
|
-
if (task.file)
|
|
620
|
-
succeededFiles.push(task.file);
|
|
621
|
-
if (isAlreadyAbsentResult(result)) {
|
|
622
|
-
taskResult.label = `${task.label} (already absent)`;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
catch (err) {
|
|
626
|
-
if (isAuthError(err)) {
|
|
627
|
-
authFailed = true;
|
|
628
|
-
authFailedTasks.push(task);
|
|
629
|
-
taskResult.status = "auth-failed";
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
632
|
-
failed++;
|
|
633
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
634
|
-
const failure = {
|
|
635
|
-
label: task.label,
|
|
636
|
-
file: task.file,
|
|
637
|
-
error: errMsg,
|
|
638
|
-
};
|
|
639
|
-
pushFailures.push(failure);
|
|
640
|
-
stepFailed.push(failure);
|
|
641
|
-
taskResult.status = "failed";
|
|
642
|
-
taskResult.error = errMsg;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
log(_jsx(Box, { paddingX: 2, children: _jsx(PushTaskLine, { result: taskResult }) }));
|
|
646
|
-
}
|
|
647
|
-
return stepFailed.length === 0 && !authFailed;
|
|
648
|
-
}
|
|
649
|
-
catch (err) {
|
|
650
|
-
// Don't swallow — a thrown error here means the loop body itself
|
|
651
|
-
// (not a per-task failure) blew up. Letting the original `} catch
|
|
652
|
-
// { return false; }` swallow it caused exit 0 with no telemetry.
|
|
653
|
-
throw err;
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
const paramTasks = pushTasks.filter((t) => t.type === "put-parameters" ||
|
|
657
|
-
t.type === "create-parameter" ||
|
|
658
|
-
t.type === "patch-parameter" ||
|
|
659
|
-
t.type === "delete-parameter");
|
|
660
|
-
const entityTasks = pushTasks.filter((t) => t.type === "put-entities" ||
|
|
661
|
-
t.type === "create-entity" ||
|
|
662
|
-
t.type === "patch-entity" ||
|
|
663
|
-
t.type === "delete-entity");
|
|
664
|
-
const integrationUpsertTasks = pushTasks.filter(isIntegrationUpsertTask);
|
|
665
|
-
const integrationDeleteTasks = pushTasks.filter((t) => t.type === "delete-integration");
|
|
666
|
-
const toolTasks = pushTasks.filter((t) => t.type === "patch-tool" ||
|
|
667
|
-
t.type === "create-tool" ||
|
|
668
|
-
t.type === "delete-tool");
|
|
669
|
-
const settingsTasks = pushTasks.filter((t) => t.type === "patch-general-settings");
|
|
670
|
-
const rulesTasks = pushTasks.filter((t) => t.type === "put-rules");
|
|
671
|
-
// ─── Push order — see file header for rationale ─────────────────────
|
|
672
|
-
//
|
|
673
|
-
// Phase 1: UPSERTS, top-down by dependency (least → most depends-on).
|
|
674
|
-
// Every step is sequential by construction (`pushStep` has no
|
|
675
|
-
// parallel mode). Do not work around this — the agent-settings
|
|
676
|
-
// backend silently merges / drops unresolvable refs on concurrent
|
|
677
|
-
// writes.
|
|
678
|
-
// 1. Parameters — referenced by entities, integrations, tools, rules.
|
|
679
|
-
await pushStep(paramTasks, "Pushing parameters");
|
|
680
|
-
// 2. Entities — reference parameters; referenced by tools, integrations.
|
|
681
|
-
await pushStep(entityTasks, "Pushing entities");
|
|
682
|
-
// 3. Integration upserts — reference parameters / entities. Must be
|
|
683
|
-
// pushed BEFORE knowledge-base uploads (KBs attach to integrations)
|
|
684
|
-
// AND before tools (tools reference integration codes).
|
|
685
|
-
await pushStep(integrationUpsertTasks, "Pushing integrations");
|
|
686
|
-
// 4. Knowledge Bases — reference integrations existing on the platform.
|
|
687
|
-
// KB failures are folded into the same `failed` counter / pushFailures
|
|
688
|
-
// array as agent-settings writes, so they hit the "X failed." line, the
|
|
689
|
-
// JSON envelope, and the non-zero exit code (BFF contract: zero silent
|
|
690
|
-
// errors anywhere in the push pipeline).
|
|
691
|
-
const kbResult = await pushKnowledgeHubs(projectRoot, projectConfig);
|
|
692
|
-
pushSpan.setAttribute("push.kb.ok", kbResult.ok);
|
|
693
|
-
pushSpan.setAttribute("push.kb.failures", kbResult.failures.length);
|
|
694
|
-
if (!kbResult.ok) {
|
|
695
|
-
pushSpan.addEvent("kb.failures_folded_into_pushFailures", {
|
|
696
|
-
count: kbResult.failures.length,
|
|
490
|
+
});
|
|
491
|
+
pushSpan.setAttribute("push.version_id", prePushDraft.versionId);
|
|
492
|
+
pushSpan.setAttribute("push.version_label", prePushDraft.label);
|
|
493
|
+
pushSpan.setAttribute("push.agent_management_id", prePushDraft.agentId);
|
|
494
|
+
// First-push migration: persist agent_management_id back to .auirc so
|
|
495
|
+
// subsequent pushes skip the listAgents lookup. Non-fatal if the
|
|
496
|
+
// write fails (we already have the id in memory for this push).
|
|
497
|
+
if (!projectConfig.agent_management_id) {
|
|
498
|
+
try {
|
|
499
|
+
saveProjectConfig({ ...projectConfig, agent_management_id: prePushDraft.agentId }, projectRoot);
|
|
500
|
+
pushSpan.addEvent("auirc.agent_management_id_persisted_from_draft", {
|
|
501
|
+
agent_management_id: prePushDraft.agentId,
|
|
697
502
|
});
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
if (process.env.AUI_DEBUG) {
|
|
506
|
+
console.warn("[debug] failed to persist agent_management_id back to .auirc:", err instanceof Error ? err.message : err);
|
|
701
507
|
}
|
|
702
508
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
509
|
+
}
|
|
510
|
+
// ─── Create API client ───
|
|
511
|
+
const client = new AUIClient({
|
|
512
|
+
baseUrl: config.apiUrl,
|
|
513
|
+
authToken: config.authToken,
|
|
514
|
+
accountId: config.accountId,
|
|
515
|
+
organizationId: config.organizationId,
|
|
516
|
+
environment: config.environment,
|
|
517
|
+
});
|
|
518
|
+
const pushLogDir = path.join(projectRoot, ".aui", "push-logs");
|
|
519
|
+
fs.mkdirSync(pushLogDir, { recursive: true });
|
|
520
|
+
client.setPushLogDir(pushLogDir);
|
|
521
|
+
if (options.apiKey) {
|
|
522
|
+
saveAgentSettingsApiKey(options.apiKey);
|
|
523
|
+
client.setAgentSettingsApiKey(options.apiKey);
|
|
524
|
+
}
|
|
525
|
+
const savedApiKey = loadAgentSettingsApiKey();
|
|
526
|
+
if (savedApiKey && !options.apiKey) {
|
|
527
|
+
client.setAgentSettingsApiKey(savedApiKey);
|
|
528
|
+
}
|
|
529
|
+
// ─── Mode dispatch: blob (new JSON push/pull) vs records (legacy) ───
|
|
530
|
+
//
|
|
531
|
+
// Every agent now carries an `Agent.bundle_mode` boolean (added
|
|
532
|
+
// 2026-05-24, see Notion "Agent Settings Push/Pull - Integration
|
|
533
|
+
// Guide" §2). The two surfaces are MUTUALLY EXCLUSIVE on the
|
|
534
|
+
// server — calling the wrong one returns `422 mode mismatch`. We
|
|
535
|
+
// detect the mode here (one `getAgent` call against an id we already
|
|
536
|
+
// have from the draft resolution above) and either continue with
|
|
537
|
+
// the blob-mode pipeline below OR hand off to the legacy per-entity
|
|
538
|
+
// pipeline in `./legacy/push-records-mode.tsx`.
|
|
539
|
+
//
|
|
540
|
+
// Why the dispatch lives here, not at the top of `_push`:
|
|
541
|
+
// - We need `agent_management_id` to look up the AgentInfo, and
|
|
542
|
+
// the cheapest way to get it (without an extra listAgents) is
|
|
543
|
+
// to wait for `resolveVersionDraft` to do its own lookup —
|
|
544
|
+
// `prePushDraft.agentId` is already populated above, and
|
|
545
|
+
// `prePushDraft.agentInfo` carries the full AgentInfo so the
|
|
546
|
+
// dispatcher can read `bundle_mode` without a second round-trip.
|
|
547
|
+
// - The legacy module is fully self-contained (it re-loads
|
|
548
|
+
// projectConfig, re-resolves the draft, re-scans files); we're
|
|
549
|
+
// paying a small duplication cost on records-mode pushes for a
|
|
550
|
+
// much cleaner separation of code paths. Records-mode is the
|
|
551
|
+
// fallback for unmigrated agents — almost all new agents are
|
|
552
|
+
// blob-mode, so the duplicate cost is rare and bounded.
|
|
553
|
+
// - The KB push below is mode-agnostic but the legacy pipeline
|
|
554
|
+
// ALREADY does its own KB push as part of its dependency-
|
|
555
|
+
// ordered flow. Dispatching BEFORE the KB block here keeps both
|
|
556
|
+
// paths' KB handling identical and avoids double-uploads.
|
|
557
|
+
const modeResolution = await detectAgentBundleMode(client, prePushDraft.agentId, prePushDraft.agentInfo);
|
|
558
|
+
pushSpan.setAttribute("push.dispatch.mode", modeResolution.mode);
|
|
559
|
+
pushSpan.setAttribute("push.dispatch.mode_source", modeResolution.source);
|
|
560
|
+
if (modeResolution.mode === "records") {
|
|
561
|
+
pushSpan.addEvent("dispatch.records_mode_handoff", {
|
|
562
|
+
agent_management_id: prePushDraft.agentId,
|
|
563
|
+
version_id: prePushDraft.versionId,
|
|
564
|
+
source: modeResolution.source,
|
|
565
|
+
});
|
|
566
|
+
// Hand off to the legacy per-entity pipeline. The legacy module
|
|
567
|
+
// re-derives everything from `options` + `agentCode` + on-disk
|
|
568
|
+
// `.auirc`, so we just forward the surface options that map
|
|
569
|
+
// cleanly. Blob-mode-only options (--agent-id, --caller,
|
|
570
|
+
// --commit-message) are intentionally dropped — they have no
|
|
571
|
+
// meaning in records-mode.
|
|
572
|
+
return runLegacyPush(pushSpan, _agentCode, {
|
|
573
|
+
force: options.force,
|
|
574
|
+
skipValidation: options.skipValidation,
|
|
575
|
+
dryRun: options.dryRun,
|
|
576
|
+
apiKey: options.apiKey,
|
|
577
|
+
versionId: options.versionId,
|
|
578
|
+
scopeLevel: options.scopeLevel,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
// ─── Knowledge-base push (separate from .aui.json bundle) ───
|
|
582
|
+
// KBs are real content assets (PDFs, etc.) under `knowledge-hubs/`,
|
|
583
|
+
// uploaded via the KB view client. KB failures fold into the same
|
|
584
|
+
// `pushFailures` array as bundle failures so they hit the JSON
|
|
585
|
+
// envelope and the non-zero exit code (BFF contract).
|
|
586
|
+
const pushFailures = [];
|
|
587
|
+
let kbFailedCount = 0;
|
|
588
|
+
const kbResult = await pushKnowledgeHubs(projectRoot, projectConfig);
|
|
589
|
+
pushSpan.setAttribute("push.kb.ok", kbResult.ok);
|
|
590
|
+
pushSpan.setAttribute("push.kb.failures", kbResult.failures.length);
|
|
591
|
+
if (!kbResult.ok) {
|
|
592
|
+
pushSpan.addEvent("kb.failures_folded_into_pushFailures", {
|
|
593
|
+
count: kbResult.failures.length,
|
|
594
|
+
});
|
|
595
|
+
for (const kbFailure of kbResult.failures) {
|
|
596
|
+
kbFailedCount++;
|
|
597
|
+
pushFailures.push(kbFailure);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// ─── Single Push call ───
|
|
601
|
+
// The new JSON endpoint expects an ASSEMBLED bundle, not files. We
|
|
602
|
+
// build the bundle from the in-memory `fileData` (already loaded
|
|
603
|
+
// and JSON-parsed earlier in this function) using the
|
|
604
|
+
// `assembleBundleFromFiles` helper, which handles both wrapped and
|
|
605
|
+
// flat shapes for each canonical file plus tool-file unwrapping.
|
|
606
|
+
const assembledBundle = assembleBundleFromFiles(fileData, {
|
|
607
|
+
schemaVersion: 1,
|
|
608
|
+
});
|
|
609
|
+
// Diagnostic counts for the spinner and span attrs. The bundle
|
|
610
|
+
// doesn't carry the raw `.aui.json` count anymore — it's domain-
|
|
611
|
+
// sliced — so report the per-domain sizes instead.
|
|
612
|
+
const bundleSummary = {
|
|
613
|
+
has_general_settings: !!assembledBundle.general_settings,
|
|
614
|
+
parameters: assembledBundle.parameters?.length ?? 0,
|
|
615
|
+
entities: assembledBundle.entities?.length ?? 0,
|
|
616
|
+
integrations: assembledBundle.integrations?.length ?? 0,
|
|
617
|
+
rules: assembledBundle.rules?.length ?? 0,
|
|
618
|
+
// `assembleBundleFromFiles` emits the tools array under the
|
|
619
|
+
// server-canonical `agent_tools` key, but historical callers
|
|
620
|
+
// may set the legacy `tools` key — count either so the span /
|
|
621
|
+
// summary doesn't lie about an "empty tools" push when tools
|
|
622
|
+
// were actually included under the legacy key.
|
|
623
|
+
tools: (assembledBundle.agent_tools?.length ?? 0) +
|
|
624
|
+
(assembledBundle.agent_tools ? 0 : assembledBundle.tools?.length ?? 0),
|
|
625
|
+
};
|
|
626
|
+
const bundleEntityCount = bundleSummary.parameters +
|
|
627
|
+
bundleSummary.entities +
|
|
628
|
+
bundleSummary.integrations +
|
|
629
|
+
bundleSummary.rules +
|
|
630
|
+
bundleSummary.tools +
|
|
631
|
+
(bundleSummary.has_general_settings ? 1 : 0);
|
|
632
|
+
const PUSH_MAX_ATTEMPTS = 4;
|
|
633
|
+
const PUSH_RETRY_BASE_MS = 1000;
|
|
634
|
+
let pushResult = null;
|
|
635
|
+
let pushAttempts = 0;
|
|
636
|
+
let lastError;
|
|
637
|
+
const pushTracer = getTracer();
|
|
638
|
+
for (let attempt = 1; attempt <= PUSH_MAX_ATTEMPTS; attempt++) {
|
|
639
|
+
pushAttempts = attempt;
|
|
640
|
+
const label = attempt === 1
|
|
641
|
+
? `Pushing bundle (${bundleEntityCount} item(s)) to draft ${prePushDraft.label}...`
|
|
642
|
+
: `Retrying push (attempt ${attempt}/${PUSH_MAX_ATTEMPTS})...`;
|
|
643
|
+
if (json)
|
|
644
|
+
stderrLog(label);
|
|
645
|
+
const attemptSpinner = json ? null : startSpinner(label);
|
|
646
|
+
const attemptOutcome = await pushTracer.startActiveSpan("aui.push.task.bundle", async (pSpan) => {
|
|
647
|
+
pSpan.setAttribute("push.task.type", "bundle");
|
|
648
|
+
pSpan.setAttribute("push.task.label", label);
|
|
649
|
+
pSpan.setAttribute("push.bundle.attempt", attempt);
|
|
650
|
+
pSpan.setAttribute("push.bundle.max_attempts", PUSH_MAX_ATTEMPTS);
|
|
651
|
+
pSpan.setAttribute("push.bundle.parameters_count", bundleSummary.parameters);
|
|
652
|
+
pSpan.setAttribute("push.bundle.entities_count", bundleSummary.entities);
|
|
653
|
+
pSpan.setAttribute("push.bundle.integrations_count", bundleSummary.integrations);
|
|
654
|
+
pSpan.setAttribute("push.bundle.rules_count", bundleSummary.rules);
|
|
655
|
+
pSpan.setAttribute("push.bundle.tools_count", bundleSummary.tools);
|
|
656
|
+
pSpan.setAttribute("push.bundle.has_general_settings", bundleSummary.has_general_settings);
|
|
657
|
+
pSpan.setAttribute("push.bundle.agent_id", prePushDraft.agentId);
|
|
658
|
+
pSpan.setAttribute("push.bundle.version_id", prePushDraft.versionId);
|
|
659
|
+
if (options.commitMessage) {
|
|
660
|
+
pSpan.setAttribute("push.bundle.commit_message_len", options.commitMessage.length);
|
|
735
661
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
662
|
+
try {
|
|
663
|
+
const result = await client.agentManagement.pushVersionBlobs(prePushDraft.agentId, prePushDraft.versionId, {
|
|
664
|
+
// `--caller` overrides the default; falls back to `cli`
|
|
665
|
+
// when not set. Server enum is strictly
|
|
666
|
+
// `agent_builder` | `ui` | `cli` — anything else 422s.
|
|
667
|
+
caller: options.caller ?? "cli",
|
|
668
|
+
commit_message: options.commitMessage,
|
|
669
|
+
// `pushed_by` (added 2026-05-25 per backend team): records
|
|
670
|
+
// the user id of the person doing the push on the new
|
|
671
|
+
// `agent_version_blob_revisions` row alongside `caller`.
|
|
672
|
+
// `caller` tells you WHICH surface invoked /push (cli /
|
|
673
|
+
// ui / agent_builder); `pushed_by` tells you WHO. Session
|
|
674
|
+
// is guaranteed non-null by the throw on line 585 above,
|
|
675
|
+
// so `session.user_id` is always populated here. Omitted
|
|
676
|
+
// only if the session somehow lacks a user_id (defensive
|
|
677
|
+
// — JWT claim should always carry one).
|
|
678
|
+
...(session.user_id ? { pushed_by: session.user_id } : {}),
|
|
679
|
+
bundle: assembledBundle,
|
|
753
680
|
});
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
for (const task of retryTasks) {
|
|
760
|
-
const retrySpinner = startSpinner(task.label);
|
|
761
|
-
try {
|
|
762
|
-
await executePushTask(client, agentSettingsParams, task);
|
|
763
|
-
succeeded++;
|
|
764
|
-
if (task.file)
|
|
765
|
-
succeededFiles.push(task.file);
|
|
766
|
-
retrySpinner.succeed(task.label);
|
|
681
|
+
if (result.success) {
|
|
682
|
+
pSpan.setStatus({ code: SpanStatusCode.OK });
|
|
683
|
+
pSpan.setAttribute("push.bundle.outcome", "success");
|
|
684
|
+
if (result.data?.new_version_tag) {
|
|
685
|
+
pSpan.setAttribute("push.bundle.new_version_tag", result.data.new_version_tag);
|
|
767
686
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
771
|
-
pushFailures.push({
|
|
772
|
-
label: task.label,
|
|
773
|
-
file: task.file,
|
|
774
|
-
error: errMsg,
|
|
775
|
-
});
|
|
776
|
-
retrySpinner.fail(task.label);
|
|
777
|
-
log(_jsx(ErrorDisplay, { error: err }));
|
|
687
|
+
if (result.data?.revision_id) {
|
|
688
|
+
pSpan.setAttribute("push.bundle.revision_id", result.data.revision_id);
|
|
778
689
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
else {
|
|
782
|
-
failed += authFailedTasks.length;
|
|
783
|
-
pushSpan.addEvent("auth.fallback.api_key_skipped", {
|
|
784
|
-
uncovered_task_count: authFailedTasks.length,
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
else if (authFailed && authFailedTasks.length > 0) {
|
|
789
|
-
failed += authFailedTasks.length;
|
|
790
|
-
pushSpan.addEvent("auth.fallback.saved_key_still_failed", {
|
|
791
|
-
failed_task_count: authFailedTasks.length,
|
|
792
|
-
});
|
|
793
|
-
log(_jsx(ErrorDisplay, { error: new AuthenticationError("Auth failed even with saved API key.", {
|
|
794
|
-
suggestion: "Try: rm ~/.aui/agent-settings-key",
|
|
795
|
-
}) }));
|
|
796
|
-
}
|
|
797
|
-
const logRelPath = path.relative(projectRoot, pushLogDir);
|
|
798
|
-
pushSpan.setAttribute("push.succeeded", succeeded);
|
|
799
|
-
pushSpan.setAttribute("push.failed", failed);
|
|
800
|
-
if (pushFailures.length > 0) {
|
|
801
|
-
pushSpan.setAttribute("push.failures", JSON.stringify(pushFailures));
|
|
802
|
-
}
|
|
803
|
-
// ─── Push Snapshot (last step — after entity push) ───
|
|
804
|
-
// The snapshot captures the local file state and uploads it as the
|
|
805
|
-
// source of truth for this push. Runs regardless of entity-push outcomes
|
|
806
|
-
// so the file history is preserved even on partial DB failures.
|
|
807
|
-
// Files = source of truth; DB updates are best-effort.
|
|
808
|
-
//
|
|
809
|
-
// IMPORTANT: snapshot failures must be loud — they are NOT counted in
|
|
810
|
-
// the entity `failed` counter, so without explicit surfacing here and
|
|
811
|
-
// a thrown CLIError at the end of `_push`, a snapshot failure would
|
|
812
|
-
// be hidden behind the "All N change(s) pushed successfully" summary
|
|
813
|
-
// and the process would exit 0.
|
|
814
|
-
// Retry policy: snapshot upload is the source-of-truth step; transient
|
|
815
|
-
// failures (network blips, 5xx, timeouts) are common with multipart
|
|
816
|
-
// uploads, so we retry up to 3 times with exponential backoff (1s, 2s,
|
|
817
|
-
// 4s) before giving up. 4xx responses are still retried — keeping the
|
|
818
|
-
// policy dumb here mirrors the single retry already in the api-client
|
|
819
|
-
// layer for entity pushes.
|
|
820
|
-
let snapshotSucceeded = false;
|
|
821
|
-
let snapshotError;
|
|
822
|
-
let snapshotAttempts = 0;
|
|
823
|
-
if (prePushDraft) {
|
|
824
|
-
const SNAPSHOT_MAX_ATTEMPTS = 4;
|
|
825
|
-
const SNAPSHOT_RETRY_BASE_MS = 1000;
|
|
826
|
-
const snapshotTracer = getTracer();
|
|
827
|
-
for (let attempt = 1; attempt <= SNAPSHOT_MAX_ATTEMPTS; attempt++) {
|
|
828
|
-
snapshotAttempts = attempt;
|
|
829
|
-
const label = attempt === 1
|
|
830
|
-
? "Pushing snapshot (file state)..."
|
|
831
|
-
: `Retrying snapshot push (attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS})...`;
|
|
832
|
-
if (json)
|
|
833
|
-
stderrLog(label);
|
|
834
|
-
const snapshotSpinner = json ? null : startSpinner(label);
|
|
835
|
-
// Per-attempt span — each snapshot upload is a network call that can
|
|
836
|
-
// hang for minutes (large multipart upload). Surfacing each attempt
|
|
837
|
-
// separately in Logfire lets us see retry behavior, attempt latency,
|
|
838
|
-
// and which attempt finally succeeded. Logfire query:
|
|
839
|
-
// `name:"aui.push.task.snapshot" AND attributes."snapshot.attempt":3`
|
|
840
|
-
// finds every push that needed a third try.
|
|
841
|
-
let attemptError;
|
|
842
|
-
const attemptResolved = await snapshotTracer.startActiveSpan("aui.push.task.snapshot", async (snapSpan) => {
|
|
843
|
-
snapSpan.setAttribute("push.task.type", "snapshot");
|
|
844
|
-
snapSpan.setAttribute("push.task.label", label);
|
|
845
|
-
snapSpan.setAttribute("snapshot.attempt", attempt);
|
|
846
|
-
snapSpan.setAttribute("snapshot.max_attempts", SNAPSHOT_MAX_ATTEMPTS);
|
|
847
|
-
snapSpan.setAttribute("snapshot.file_count", fileData.length);
|
|
848
|
-
snapSpan.setAttribute("snapshot.agent_id", prePushDraft.agentId);
|
|
849
|
-
snapSpan.setAttribute("snapshot.version_id", prePushDraft.versionId);
|
|
850
|
-
try {
|
|
851
|
-
const snapshotResult = await pushSnapshot(client, prePushDraft.agentId, prePushDraft.versionId, projectRoot, fileData);
|
|
852
|
-
if (snapshotResult.success) {
|
|
853
|
-
snapSpan.setStatus({ code: SpanStatusCode.OK });
|
|
854
|
-
snapSpan.setAttribute("snapshot.outcome", "success");
|
|
855
|
-
return { ok: true, error: undefined };
|
|
856
|
-
}
|
|
857
|
-
const errMsg = snapshotResult.error || "Unknown snapshot error";
|
|
858
|
-
snapSpan.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
859
|
-
snapSpan.setAttribute("snapshot.outcome", "failed");
|
|
860
|
-
snapSpan.setAttribute("push.task.error", errMsg);
|
|
861
|
-
if (attempt < SNAPSHOT_MAX_ATTEMPTS) {
|
|
862
|
-
snapSpan.addEvent("snapshot.retry_will_follow", {
|
|
863
|
-
next_attempt: attempt + 1,
|
|
864
|
-
backoff_ms: SNAPSHOT_RETRY_BASE_MS * Math.pow(2, attempt - 1),
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
return { ok: false, error: errMsg };
|
|
690
|
+
if (result.data?.gcs_key) {
|
|
691
|
+
pSpan.setAttribute("push.bundle.gcs_key", result.data.gcs_key);
|
|
868
692
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
snapSpan.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
872
|
-
snapSpan.recordException(error instanceof Error ? error : new Error(errMsg));
|
|
873
|
-
snapSpan.setAttribute("snapshot.outcome", "exception");
|
|
874
|
-
snapSpan.setAttribute("push.task.error", errMsg);
|
|
875
|
-
return { ok: false, error: errMsg };
|
|
693
|
+
if (result.data?.sha256) {
|
|
694
|
+
pSpan.setAttribute("push.bundle.sha256", result.data.sha256);
|
|
876
695
|
}
|
|
877
|
-
|
|
878
|
-
|
|
696
|
+
if (typeof result.data?.size === "number") {
|
|
697
|
+
pSpan.setAttribute("push.bundle.size_bytes", result.data.size);
|
|
879
698
|
}
|
|
880
|
-
|
|
881
|
-
if (attemptResolved.ok) {
|
|
882
|
-
const okMsg = attempt === 1
|
|
883
|
-
? `Snapshot pushed (${fileData.length} file(s))`
|
|
884
|
-
: `Snapshot pushed (${fileData.length} file(s), attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS})`;
|
|
885
|
-
if (snapshotSpinner)
|
|
886
|
-
snapshotSpinner.succeed(okMsg);
|
|
887
|
-
else
|
|
888
|
-
stderrLog(okMsg);
|
|
889
|
-
snapshotSucceeded = true;
|
|
890
|
-
snapshotError = undefined;
|
|
891
|
-
break;
|
|
699
|
+
return { ok: true, result };
|
|
892
700
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
: `Snapshot push failed (attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS}): ${attemptError}`;
|
|
899
|
-
if (snapshotSpinner)
|
|
900
|
-
snapshotSpinner.fail(failMsg);
|
|
901
|
-
else
|
|
902
|
-
stderrLog(failMsg);
|
|
903
|
-
if (isLast)
|
|
904
|
-
break;
|
|
905
|
-
const delayMs = SNAPSHOT_RETRY_BASE_MS * Math.pow(2, attempt - 1);
|
|
906
|
-
if (json)
|
|
907
|
-
stderrLog(`Waiting ${delayMs}ms before retry...`);
|
|
908
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
909
|
-
}
|
|
910
|
-
pushSpan.setAttribute("push.snapshot.success", snapshotSucceeded);
|
|
911
|
-
pushSpan.setAttribute("push.snapshot.attempts", snapshotAttempts);
|
|
912
|
-
if (!snapshotSucceeded && snapshotError) {
|
|
913
|
-
pushSpan.setAttribute("push.snapshot.error", snapshotError);
|
|
701
|
+
const errMsg = result.error || "Unknown push error";
|
|
702
|
+
pSpan.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
703
|
+
pSpan.setAttribute("push.bundle.outcome", "failed");
|
|
704
|
+
pSpan.setAttribute("push.task.error", errMsg);
|
|
705
|
+
return { ok: false, error: errMsg, result };
|
|
914
706
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
?
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
// web-search and a failed POST on search-flights), do NOT commit that
|
|
931
|
-
// file to baseline. If we did, the next push's git diff would treat the
|
|
932
|
-
// failed item as "already on the platform" and emit a PATCH that 404s.
|
|
933
|
-
// The previous behaviour stuck users in an unrecoverable retry loop.
|
|
934
|
-
let baselineUpdated = false;
|
|
935
|
-
const canCommitBaseline = !prePushDraft || snapshotSucceeded;
|
|
936
|
-
if (canCommitBaseline) {
|
|
937
|
-
// A file is committable iff EVERY task that targeted it succeeded.
|
|
938
|
-
// Build the failed-files set from `pushFailures` (which now includes
|
|
939
|
-
// both agent-settings entity failures AND knowledge-hub failures —
|
|
940
|
-
// see the KB push step).
|
|
941
|
-
const failedFiles = new Set();
|
|
942
|
-
for (const f of pushFailures) {
|
|
943
|
-
if (f.file)
|
|
944
|
-
failedFiles.add(f.file);
|
|
945
|
-
}
|
|
946
|
-
const filesSafeToCommit = succeededFiles.filter((f) => !failedFiles.has(f));
|
|
947
|
-
if (failed > 0 && succeeded > 0) {
|
|
948
|
-
if (filesSafeToCommit.length > 0) {
|
|
949
|
-
commitBaselineFiles(projectRoot, filesSafeToCommit, `pushed ${succeeded} change(s) (${failedFiles.size} file(s) held back due to per-task failures)`);
|
|
950
|
-
baselineUpdated = true;
|
|
951
|
-
pushSpan.addEvent("baseline.partial_commit", {
|
|
952
|
-
committed_files: filesSafeToCommit.length,
|
|
953
|
-
held_back_files: failedFiles.size,
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
else {
|
|
957
|
-
pushSpan.addEvent("baseline.fully_held_back", {
|
|
958
|
-
failed_files: failedFiles.size,
|
|
707
|
+
catch (error) {
|
|
708
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
709
|
+
pSpan.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
710
|
+
pSpan.recordException(error instanceof Error ? error : new Error(errMsg));
|
|
711
|
+
pSpan.setAttribute("push.bundle.outcome", "exception");
|
|
712
|
+
pSpan.setAttribute("push.task.error", errMsg);
|
|
713
|
+
// Server changed the agent's mode mid-flight (rare: usually
|
|
714
|
+
// an admin flipping `bundle_mode` while a push is in
|
|
715
|
+
// progress). The 422 is unrecoverable on this surface —
|
|
716
|
+
// translate to a clear "rerun" hint instead of letting it
|
|
717
|
+
// look like a transient push error.
|
|
718
|
+
if (isModeMismatchError(error)) {
|
|
719
|
+
pSpan.setAttribute("push.bundle.mode_mismatch", true);
|
|
720
|
+
throw new CLIError(`Server reports this agent is now in DB-records mode — the JSON push endpoint rejected the request (422).`, {
|
|
721
|
+
suggestion: "Re-run `aui push`. The CLI re-detects the agent's mode on every invocation and will pick the legacy per-entity pipeline on the next run.",
|
|
959
722
|
});
|
|
960
723
|
}
|
|
724
|
+
return { ok: false, error: errMsg, result: null };
|
|
961
725
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
baselineUpdated = true;
|
|
965
|
-
pushSpan.addEvent("baseline.full_commit");
|
|
726
|
+
finally {
|
|
727
|
+
pSpan.end();
|
|
966
728
|
}
|
|
729
|
+
});
|
|
730
|
+
if (attemptOutcome.ok) {
|
|
731
|
+
const newTag = attemptOutcome.result.data?.new_version_tag ?? prePushDraft.label;
|
|
732
|
+
const okMsg = attempt === 1
|
|
733
|
+
? `Push complete → ${newTag} (bundle: ${bundleEntityCount} item(s))`
|
|
734
|
+
: `Push complete → ${newTag} (bundle: ${bundleEntityCount} item(s), attempt ${attempt}/${PUSH_MAX_ATTEMPTS})`;
|
|
735
|
+
if (attemptSpinner)
|
|
736
|
+
attemptSpinner.succeed(okMsg);
|
|
737
|
+
else
|
|
738
|
+
stderrLog(okMsg);
|
|
739
|
+
pushResult = attemptOutcome.result;
|
|
740
|
+
lastError = undefined;
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
lastError = attemptOutcome.error;
|
|
744
|
+
// 4xx are deterministic; only retry 5xx + 409 (lock contention).
|
|
745
|
+
const shouldRetry = isTransientPushError(attemptOutcome.error) && attempt < PUSH_MAX_ATTEMPTS;
|
|
746
|
+
const isLast = !shouldRetry;
|
|
747
|
+
const failMsg = isLast
|
|
748
|
+
? `Push failed after ${attempt} attempt(s): ${attemptOutcome.error}`
|
|
749
|
+
: `Push failed (attempt ${attempt}/${PUSH_MAX_ATTEMPTS}): ${attemptOutcome.error}`;
|
|
750
|
+
if (attemptSpinner)
|
|
751
|
+
attemptSpinner.fail(failMsg);
|
|
752
|
+
else
|
|
753
|
+
stderrLog(failMsg);
|
|
754
|
+
if (isLast) {
|
|
755
|
+
pushResult = attemptOutcome.result;
|
|
756
|
+
break;
|
|
967
757
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
758
|
+
const delayMs = PUSH_RETRY_BASE_MS * Math.pow(2, attempt - 1);
|
|
759
|
+
if (json)
|
|
760
|
+
stderrLog(`Waiting ${delayMs}ms before retry...`);
|
|
761
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
762
|
+
}
|
|
763
|
+
const pushSucceeded = !!pushResult && pushResult.success;
|
|
764
|
+
pushSpan.setAttribute("push.bundle.success", pushSucceeded);
|
|
765
|
+
pushSpan.setAttribute("push.bundle.attempts", pushAttempts);
|
|
766
|
+
if (!pushSucceeded && lastError) {
|
|
767
|
+
pushSpan.setAttribute("push.bundle.error", lastError);
|
|
768
|
+
pushFailures.push({
|
|
769
|
+
label: `Push bundle to ${prePushDraft.label}`,
|
|
770
|
+
error: lastError,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
// ─── Persist new revision tag to .auirc ───
|
|
774
|
+
// The backend assigns a fresh `version_tag` on every push (e.g.
|
|
775
|
+
// v8.13 → v8.14). Save it to `version_tag` (NOT `version_label`,
|
|
776
|
+
// which represents the version document's `version_number` — e.g.
|
|
777
|
+
// "v8" — and is used for telemetry / version UX). On the next
|
|
778
|
+
// `aui pull` we use this tag as the default `?version_tag=…` query
|
|
779
|
+
// param so users re-pull exactly the revision they just pushed.
|
|
780
|
+
if (pushSucceeded && pushResult?.data) {
|
|
781
|
+
const newTag = pushResult.data.new_version_tag;
|
|
782
|
+
if (newTag) {
|
|
783
|
+
try {
|
|
784
|
+
saveProjectConfig({ ...projectConfig, version_tag: newTag }, projectRoot);
|
|
785
|
+
pushSpan.addEvent("auirc.version_tag_persisted", { tag: newTag });
|
|
983
786
|
}
|
|
984
|
-
|
|
985
|
-
|
|
787
|
+
catch (err) {
|
|
788
|
+
if (process.env.AUI_DEBUG) {
|
|
789
|
+
console.warn("[debug] failed to persist version_tag to .auirc:", err instanceof Error ? err.message : err);
|
|
790
|
+
}
|
|
986
791
|
}
|
|
987
792
|
}
|
|
988
|
-
const snapshotFailed = snapshotStatus === "failed";
|
|
989
|
-
pushSpan.setAttribute("push.exit_reason", failed > 0
|
|
990
|
-
? succeeded > 0
|
|
991
|
-
? "partial_failure"
|
|
992
|
-
: "failed"
|
|
993
|
-
: snapshotFailed
|
|
994
|
-
? "snapshot_failed"
|
|
995
|
-
: "completed");
|
|
996
|
-
pushSpan.setAttribute("push.succeeded_count", succeeded);
|
|
997
|
-
pushSpan.setAttribute("push.failed_count", failed);
|
|
998
|
-
// ─── Final structured JSON envelope (success path) ───
|
|
999
|
-
// The BFF / CI consumes --json output programmatically. On success we
|
|
1000
|
-
// emit a single envelope at the very end so the caller can parse one
|
|
1001
|
-
// top-level JSON document with the full push outcome. Failure paths
|
|
1002
|
-
// are handled by handleError() via outputJsonError().
|
|
1003
|
-
if (json && !snapshotFailed && failed === 0) {
|
|
1004
|
-
outputJson({
|
|
1005
|
-
agent: {
|
|
1006
|
-
code: agentCodeStr,
|
|
1007
|
-
id: agentIdStr,
|
|
1008
|
-
},
|
|
1009
|
-
version: prePushDraft
|
|
1010
|
-
? { id: prePushDraft.versionId, label: prePushDraft.label }
|
|
1011
|
-
: undefined,
|
|
1012
|
-
succeeded_count: succeeded,
|
|
1013
|
-
failed_count: 0,
|
|
1014
|
-
succeeded_files: succeededFiles,
|
|
1015
|
-
snapshot: snapshotStatus
|
|
1016
|
-
? {
|
|
1017
|
-
status: snapshotStatus,
|
|
1018
|
-
attempts: snapshotAttempts,
|
|
1019
|
-
}
|
|
1020
|
-
: undefined,
|
|
1021
|
-
baseline_updated: baselineUpdated,
|
|
1022
|
-
log_dir: logRelPath,
|
|
1023
|
-
memory_path: memoryPath,
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
// ─── Failure throws (loud + non-zero exit + JSON error envelope) ───
|
|
1027
|
-
// Snapshot failure takes priority because if the snapshot doesn't land
|
|
1028
|
-
// the version's file history is incomplete, even when individual DB
|
|
1029
|
-
// updates succeeded. Falls through to handleError() which prints the
|
|
1030
|
-
// formatted error and exits non-zero.
|
|
1031
|
-
if (snapshotFailed) {
|
|
1032
|
-
throw new CLIError(`Snapshot upload failed${snapshotError ? `: ${snapshotError}` : ""}`, {
|
|
1033
|
-
suggestion: "Re-run `aui push` to retry the snapshot. Your local files are the source of truth — they remain unchanged.",
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
// Entity-task partial / total failure. Without this throw, `aui push`
|
|
1037
|
-
// would print "X failed" to stdout and exit 0 — the BFF would have no
|
|
1038
|
-
// way to know the push didn't fully apply, and Logfire would record a
|
|
1039
|
-
// "completed" span — a real reported failure mode.
|
|
1040
|
-
if (failed > 0) {
|
|
1041
|
-
const labels = pushFailures.map((f) => f.label).join(", ");
|
|
1042
|
-
throw new CLIError(`${failed} entity push task(s) failed: ${labels}`, {
|
|
1043
|
-
suggestion: succeeded > 0
|
|
1044
|
-
? `${succeeded} change(s) were saved. Fix the errors above and re-run \`aui push\` to retry the rest.`
|
|
1045
|
-
: "Fix the errors above and re-run `aui push`.",
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
1048
793
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
794
|
+
// ─── Baseline commit ───
|
|
795
|
+
// The new endpoint is atomic for the bundle, so we either commit
|
|
796
|
+
// everything (bundle pushed AND KBs OK) or hold the baseline (so the
|
|
797
|
+
// user can re-run `aui push` to retry against the same pre-push state).
|
|
798
|
+
let baselineUpdated = false;
|
|
799
|
+
if (pushSucceeded && kbFailedCount === 0) {
|
|
800
|
+
commitBaseline(projectRoot, `pushed bundle → ${pushResult?.data?.new_version_tag ?? prePushDraft.label}`);
|
|
801
|
+
baselineUpdated = true;
|
|
802
|
+
pushSpan.addEvent("baseline.full_commit");
|
|
803
|
+
}
|
|
804
|
+
else if (pushSucceeded && kbFailedCount > 0) {
|
|
805
|
+
pushSpan.addEvent("baseline.held_back_due_to_kb_failure", {
|
|
806
|
+
kb_failed: kbFailedCount,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
pushSpan.addEvent("baseline.held_back_due_to_push_failure");
|
|
811
|
+
}
|
|
812
|
+
pushSpan.setAttribute("push.baseline_updated", baselineUpdated);
|
|
813
|
+
// ─── Push memory + final summary ───
|
|
814
|
+
const logRelPath = path.relative(projectRoot, pushLogDir);
|
|
815
|
+
const agentCodeStr = projectConfig.agent_code || projectConfig.agent_id || "unknown";
|
|
816
|
+
const agentIdStr = projectConfig.agent_id || "unknown";
|
|
817
|
+
const memoryPath = writePushMemory(projectRoot, {
|
|
818
|
+
agentCode: agentCodeStr,
|
|
819
|
+
agentId: agentIdStr,
|
|
820
|
+
versionLabel: prePushDraft.label,
|
|
821
|
+
newVersionTag: pushResult?.data?.new_version_tag,
|
|
822
|
+
revisionId: pushResult?.data?.revision_id,
|
|
823
|
+
commitMessage: pushResult?.data?.commit_message ?? options.commitMessage ?? undefined,
|
|
824
|
+
fileData,
|
|
825
|
+
// The new endpoint is bundle-shaped, so there isn't really a
|
|
826
|
+
// per-file upload list anymore — record the local file paths the
|
|
827
|
+
// bundle was assembled from instead. Diagnostic only.
|
|
828
|
+
uploadedFiles: fileData.map((fd) => fd.file),
|
|
829
|
+
diff,
|
|
830
|
+
pushSucceeded,
|
|
831
|
+
bundleError: pushSucceeded ? undefined : lastError,
|
|
832
|
+
kbFailures: kbResult.failures,
|
|
833
|
+
});
|
|
834
|
+
log(_jsx(PushFinalSummary, { succeeded: pushSucceeded ? bundleEntityCount : 0, failed: pushFailures.length, baselineUpdated: baselineUpdated, logDir: logRelPath, memoryPath: memoryPath, snapshotStatus: pushSucceeded ? "succeeded" : "failed", snapshotError: lastError }));
|
|
835
|
+
if (pushFailures.length > 0) {
|
|
836
|
+
log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "warning", label: `${pushFailures.length} task(s) failed.` }), pushFailures.map((f) => (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "red", children: [" ", icons.error, " ", f.label] }), _jsxs(Text, { color: colors.muted, children: [" Error: ", f.error] }), f.file && _jsxs(Text, { color: colors.muted, children: [" File: ", f.file] })] }, f.label))), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next: " }), _jsxs(Text, { color: colors.muted, children: ["Fix the issues above and re-run ", _jsx(Text, { bold: true, children: "aui push" }), " to retry. Your local files remain the source of truth."] })] })] }));
|
|
837
|
+
}
|
|
838
|
+
else if (pushSucceeded) {
|
|
839
|
+
const newTag = pushResult?.data?.new_version_tag ?? prePushDraft.label;
|
|
840
|
+
log(_jsx(StatusLine, { kind: "info", label: `All changes pushed to draft ${newTag}. Run: aui version publish → aui version activate` }));
|
|
841
|
+
}
|
|
842
|
+
// ─── Telemetry exit reason ───
|
|
843
|
+
pushSpan.setAttribute("push.exit_reason", pushSucceeded
|
|
844
|
+
? kbFailedCount > 0
|
|
845
|
+
? "partial_kb_failure"
|
|
846
|
+
: "completed"
|
|
847
|
+
: "failed");
|
|
848
|
+
pushSpan.setAttribute("push.succeeded_count", pushSucceeded ? bundleEntityCount : 0);
|
|
849
|
+
pushSpan.setAttribute("push.failed_count", pushFailures.length);
|
|
850
|
+
if (pushFailures.length > 0) {
|
|
851
|
+
pushSpan.setAttribute("push.failures", JSON.stringify(pushFailures));
|
|
852
|
+
}
|
|
853
|
+
// ─── JSON envelope (success path) ───
|
|
854
|
+
if (json && pushSucceeded && kbFailedCount === 0) {
|
|
855
|
+
outputJson({
|
|
856
|
+
agent: { code: agentCodeStr, id: agentIdStr },
|
|
857
|
+
version: {
|
|
858
|
+
id: prePushDraft.versionId,
|
|
859
|
+
label: prePushDraft.label,
|
|
860
|
+
new_tag: pushResult?.data?.new_version_tag,
|
|
861
|
+
},
|
|
862
|
+
bundle: {
|
|
863
|
+
status: "succeeded",
|
|
864
|
+
attempts: pushAttempts,
|
|
865
|
+
// No more per-file upload list — the new endpoint writes one
|
|
866
|
+
// canonical `bundle.json` and returns its key + hash + size.
|
|
867
|
+
item_count: bundleEntityCount,
|
|
868
|
+
item_counts: bundleSummary,
|
|
869
|
+
new_version_tag: pushResult?.data?.new_version_tag,
|
|
870
|
+
new_version_revision_number: pushResult?.data?.new_version_revision_number,
|
|
871
|
+
revision_id: pushResult?.data?.revision_id,
|
|
872
|
+
commit_message: pushResult?.data?.commit_message,
|
|
873
|
+
gcs_key: pushResult?.data?.gcs_key,
|
|
874
|
+
sha256: pushResult?.data?.sha256,
|
|
875
|
+
size_bytes: pushResult?.data?.size,
|
|
876
|
+
created_at: pushResult?.data?.created_at,
|
|
877
|
+
},
|
|
878
|
+
baseline_updated: baselineUpdated,
|
|
879
|
+
log_dir: logRelPath,
|
|
880
|
+
memory_path: memoryPath,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
// ─── Failure throws (loud + non-zero exit + JSON error envelope) ───
|
|
884
|
+
// CLIError funnels into handleError() which prints the formatted error,
|
|
885
|
+
// emits the JSON error envelope (in --json mode), and exits non-zero.
|
|
886
|
+
// The BFF parses both signals (HTTP status + JSON envelope) so a silent
|
|
887
|
+
// exit-0 here would let `agent-builder-bff` claim a phantom success.
|
|
888
|
+
if (!pushSucceeded) {
|
|
889
|
+
throw new CLIError(`Push failed: ${lastError ?? "unknown error"}`, {
|
|
890
|
+
suggestion: "Fix the errors above and re-run `aui push`. Your local files remain the source of truth.",
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
if (kbFailedCount > 0) {
|
|
894
|
+
const labels = pushFailures.map((f) => f.label).join(", ");
|
|
895
|
+
throw new CLIError(`${kbFailedCount} knowledge-base task(s) failed: ${labels}`, {
|
|
896
|
+
suggestion: "Re-run `aui push` to retry KB uploads. The .aui.json bundle has been saved.",
|
|
897
|
+
});
|
|
1063
898
|
}
|
|
1064
899
|
}
|
|
900
|
+
// ─── Transient error classifier (retry decision) ───
|
|
901
|
+
//
|
|
902
|
+
// The new push endpoint surfaces failures as `{ success: false, error: "<status>: <msg>" }`
|
|
903
|
+
// or as a thrown error with the same shape. We retry 5xx (network /
|
|
904
|
+
// backend hiccup) and 409 (concurrent push lock contention) — both are
|
|
905
|
+
// expected to resolve on backoff. 4xx (other than 409) are deterministic
|
|
906
|
+
// — validation / auth / missing-resource — and never get retried.
|
|
907
|
+
function isTransientPushError(err) {
|
|
908
|
+
if (!err)
|
|
909
|
+
return false;
|
|
910
|
+
// Errors come back as "<status>: <body>" from the api client, or as
|
|
911
|
+
// raw fetch errors / timeouts (no leading status). Treat fetch/abort
|
|
912
|
+
// errors as transient too — they're not the user's fault.
|
|
913
|
+
const m = err.match(/^(\d{3})/);
|
|
914
|
+
if (m) {
|
|
915
|
+
const status = parseInt(m[1], 10);
|
|
916
|
+
if (status === 409)
|
|
917
|
+
return true;
|
|
918
|
+
if (status >= 500 && status <= 599)
|
|
919
|
+
return true;
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
// No HTTP status → likely network/timeout. Retry.
|
|
923
|
+
return /timeout|timed out|fetch failed|network|econnreset|enotfound|eai_again|socket hang up/i.test(err);
|
|
924
|
+
}
|
|
1065
925
|
/**
|
|
1066
926
|
* Lookup the agent-management record for the current `.auirc` project
|
|
1067
|
-
* (preferred) or the active session fallback
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
927
|
+
* (preferred) or the active session fallback. Each attempt records its
|
|
928
|
+
* error so callers can surface the full picture instead of silently
|
|
929
|
+
* dropping `agent_id` from request bodies.
|
|
1070
930
|
*/
|
|
1071
931
|
async function lookupAgentManagementInfoForPush(config, projectConfig, session) {
|
|
1072
932
|
const client = new AUIClient({
|
|
@@ -1082,9 +942,24 @@ async function lookupAgentManagementInfoForPush(config, projectConfig, session)
|
|
|
1082
942
|
let agentInfo;
|
|
1083
943
|
const errors = [];
|
|
1084
944
|
const agentMgmtId = session.agent_management_id;
|
|
945
|
+
const projectAgentMgmtId = projectConfig.agent_management_id;
|
|
1085
946
|
const projectNetworkId = projectConfig.agent_id;
|
|
1086
947
|
const fallbackNetworkId = session.network_id;
|
|
1087
|
-
if (
|
|
948
|
+
// Highest-priority shortcut: if `.auirc` (or the `--agent-id`
|
|
949
|
+
// overlay in `_push`) already carries an `agent_management_id`,
|
|
950
|
+
// hit `getAgent` directly. One round-trip, no list scan.
|
|
951
|
+
if (projectAgentMgmtId) {
|
|
952
|
+
try {
|
|
953
|
+
agentInfo = await client.agentManagement.getAgent(projectAgentMgmtId);
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
errors.push(`getAgent(${projectAgentMgmtId}) threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
957
|
+
if (process.env.AUI_DEBUG) {
|
|
958
|
+
console.warn(`[debug] lookupAgentManagementInfoForPush: getAgent(${projectAgentMgmtId}) failed (stale id?):`, err instanceof Error ? err.message : err);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (!agentInfo && projectNetworkId) {
|
|
1088
963
|
try {
|
|
1089
964
|
const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: projectNetworkId });
|
|
1090
965
|
agentInfo = resp.items.find((a) => a.scope.network_id === projectNetworkId ||
|
|
@@ -1094,18 +969,14 @@ async function lookupAgentManagementInfoForPush(config, projectConfig, session)
|
|
|
1094
969
|
}
|
|
1095
970
|
}
|
|
1096
971
|
catch (err) {
|
|
1097
|
-
// Accumulate into `errors` so the eventual ConfigError can list every
|
|
1098
|
-
// resolution path that failed (alboim's a117251). Also emit AUI_DEBUG
|
|
1099
|
-
// warning for live operator observability (zero silent errors policy).
|
|
1100
972
|
errors.push(`listAgents(network_id=${projectNetworkId}) threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
1101
973
|
if (process.env.AUI_DEBUG) {
|
|
1102
974
|
console.warn(`[debug] resolveVersionDraft: listAgents(network_id=${projectNetworkId}) failed:`, err instanceof Error ? err.message : err);
|
|
1103
975
|
}
|
|
1104
976
|
}
|
|
1105
977
|
}
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
// the case where listAgents fell through above.
|
|
978
|
+
// Direct getAgent call by session's agent_management_id — no list scan,
|
|
979
|
+
// and gracefully covers the case where listAgents fell through.
|
|
1109
980
|
if (!agentInfo && agentMgmtId) {
|
|
1110
981
|
try {
|
|
1111
982
|
agentInfo = await client.agentManagement.getAgent(agentMgmtId);
|
|
@@ -1117,7 +988,9 @@ async function lookupAgentManagementInfoForPush(config, projectConfig, session)
|
|
|
1117
988
|
}
|
|
1118
989
|
}
|
|
1119
990
|
}
|
|
1120
|
-
if (!agentInfo &&
|
|
991
|
+
if (!agentInfo &&
|
|
992
|
+
fallbackNetworkId &&
|
|
993
|
+
fallbackNetworkId !== projectNetworkId) {
|
|
1121
994
|
try {
|
|
1122
995
|
const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: fallbackNetworkId });
|
|
1123
996
|
agentInfo = resp.items.find((a) => a.scope.network_id === fallbackNetworkId ||
|
|
@@ -1136,37 +1009,83 @@ async function lookupAgentManagementInfoForPush(config, projectConfig, session)
|
|
|
1136
1009
|
return { agentInfo, errors };
|
|
1137
1010
|
}
|
|
1138
1011
|
/**
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1012
|
+
* Dual-shape resolve for `--agent-id <id>` on `aui push`. Same pattern
|
|
1013
|
+
* as `aui import`'s explicit-id branch: the user might pass either an
|
|
1014
|
+
* `agent_management_id` (preferred, new world) or a legacy `network_id`
|
|
1015
|
+
* (back-compat). Same regex, different document family. Try
|
|
1016
|
+
* `agentManagement.getAgent(id)` first, fall back to
|
|
1017
|
+
* `listAgents(network_id=id)` on 404. Non-404 errors bubble.
|
|
1145
1018
|
*/
|
|
1146
|
-
async function
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1019
|
+
async function resolveExplicitAgentForPush(client, explicitId) {
|
|
1020
|
+
// 1) Treat as agent_management_id.
|
|
1021
|
+
try {
|
|
1022
|
+
const info = await client.agentManagement.getAgent(explicitId);
|
|
1023
|
+
// Bias the client scope toward the agent's own org/account so the
|
|
1024
|
+
// subsequent listVersions / push calls don't 403 against the
|
|
1025
|
+
// session's default scope.
|
|
1026
|
+
const scopeUpdate = {};
|
|
1027
|
+
if (info.scope?.account_id)
|
|
1028
|
+
scopeUpdate.accountId = info.scope.account_id;
|
|
1029
|
+
if (info.scope?.organization_id)
|
|
1030
|
+
scopeUpdate.organizationId = info.scope.organization_id;
|
|
1031
|
+
if (scopeUpdate.accountId || scopeUpdate.organizationId) {
|
|
1032
|
+
client.setScope(scopeUpdate);
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
agentManagementId: info.id,
|
|
1036
|
+
networkId: info.scope?.network_id ?? undefined,
|
|
1037
|
+
via: "agent_management_id",
|
|
1038
|
+
};
|
|
1155
1039
|
}
|
|
1156
|
-
|
|
1040
|
+
catch (err) {
|
|
1041
|
+
const status = err instanceof AUIAPIError
|
|
1042
|
+
? err.status
|
|
1043
|
+
: (err.statusCode ??
|
|
1044
|
+
err.status);
|
|
1045
|
+
if (status !== 404) {
|
|
1046
|
+
throw new CLIError(`Could not resolve --agent-id ${explicitId} as agent_management_id: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1047
|
+
suggestion: "Check your auth (`aui status`) and the agent id. If the id is a legacy network_id, it will be tried as a fallback only on 404 from agent-management.",
|
|
1048
|
+
cause: err,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
if (process.env.AUI_DEBUG) {
|
|
1052
|
+
console.log(`[debug] resolveExplicitAgentForPush: getAgent(${explicitId}) → 404, trying as network_id`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
// 2) Fall back: treat as legacy network_id.
|
|
1157
1056
|
try {
|
|
1158
|
-
|
|
1057
|
+
const resp = await client.agentManagement.listAgents(client.getOrganizationId(), 1, 50, { network_id: explicitId });
|
|
1058
|
+
const match = resp.items.find((a) => a.scope.network_id === explicitId || a.id === explicitId);
|
|
1059
|
+
if (!match) {
|
|
1060
|
+
throw new CLIError(`Could not resolve --agent-id ${explicitId} as agent_management_id OR network_id.`, {
|
|
1061
|
+
suggestion: "Verify the agent exists in agent-management. Run `aui agents` to list available agents.",
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
const scopeUpdate = {};
|
|
1065
|
+
if (match.scope?.account_id)
|
|
1066
|
+
scopeUpdate.accountId = match.scope.account_id;
|
|
1067
|
+
if (match.scope?.organization_id)
|
|
1068
|
+
scopeUpdate.organizationId = match.scope.organization_id;
|
|
1069
|
+
if (scopeUpdate.accountId || scopeUpdate.organizationId) {
|
|
1070
|
+
client.setScope(scopeUpdate);
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
agentManagementId: match.id,
|
|
1074
|
+
networkId: match.scope?.network_id ?? undefined,
|
|
1075
|
+
via: "network_id",
|
|
1076
|
+
};
|
|
1159
1077
|
}
|
|
1160
|
-
catch {
|
|
1161
|
-
|
|
1078
|
+
catch (err) {
|
|
1079
|
+
if (err instanceof CLIError)
|
|
1080
|
+
throw err;
|
|
1081
|
+
throw new CLIError(`Failed to resolve --agent-id ${explicitId} via legacy network_id lookup: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1162
1082
|
}
|
|
1163
|
-
return agentInfo.id;
|
|
1164
1083
|
}
|
|
1165
1084
|
async function resolveVersionDraft(config, projectConfig, session, explicitVersionId) {
|
|
1166
1085
|
// Every error path below MUST throw a typed CLIError (not return null).
|
|
1167
|
-
// Returning null silently exits
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1086
|
+
// Returning null silently exits with code 0 — the BFF then thinks the
|
|
1087
|
+
// push succeeded and the failure never reaches Logfire because no
|
|
1088
|
+
// exception bubbled to handleError.
|
|
1170
1089
|
const { agentInfo, errors: lookupErrors } = await lookupAgentManagementInfoForPush(config, projectConfig, session);
|
|
1171
1090
|
if (!agentInfo) {
|
|
1172
1091
|
const detail = lookupErrors.length > 0 ? `\n - ${lookupErrors.join("\n - ")}` : "";
|
|
@@ -1184,17 +1103,88 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
|
|
|
1184
1103
|
const key = loadAgentSettingsApiKey();
|
|
1185
1104
|
if (key)
|
|
1186
1105
|
client.setAgentSettingsApiKey(key);
|
|
1187
|
-
// If user passed --version-id, validate it's a draft
|
|
1106
|
+
// If user passed --version-id, validate it's a draft.
|
|
1188
1107
|
if (explicitVersionId) {
|
|
1189
1108
|
let ver;
|
|
1109
|
+
let owningAgent = agentInfo;
|
|
1190
1110
|
try {
|
|
1191
1111
|
ver = await client.agentManagement.getVersion(agentInfo.id, explicitVersionId);
|
|
1192
1112
|
}
|
|
1193
1113
|
catch (error) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1114
|
+
const status = error.statusCode ??
|
|
1115
|
+
error.status;
|
|
1116
|
+
if (status !== 404) {
|
|
1117
|
+
throw new CLIError(`Could not fetch version "${explicitVersionId}": ${error instanceof Error ? error.message : String(error)}`, {
|
|
1118
|
+
suggestion: "Check the version ID with `aui version list` and try again.",
|
|
1119
|
+
cause: error,
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
// ─── Multi-agent fallback (same pattern as `selectVersionForAgent`)
|
|
1123
|
+
//
|
|
1124
|
+
// One `network_id` can map to multiple agent-management records
|
|
1125
|
+
// (data-migration duplicates, account-scoped variants, etc.).
|
|
1126
|
+
// We picked the first match from `lookupAgentManagementInfoForPush`
|
|
1127
|
+
// above, but `--version-id` may belong to a SIBLING agent record
|
|
1128
|
+
// for the same network. Paginate every candidate for the
|
|
1129
|
+
// network and try `getVersion` on each — return the first one
|
|
1130
|
+
// that succeeds with the matching version, and use THAT agent's
|
|
1131
|
+
// id when pushing so the bundle lands on the correct revision.
|
|
1132
|
+
const projectNetworkId = projectConfig.agent_id;
|
|
1133
|
+
if (projectNetworkId) {
|
|
1134
|
+
const probeClient = new AUIClient({
|
|
1135
|
+
baseUrl: config.apiUrl,
|
|
1136
|
+
authToken: config.authToken,
|
|
1137
|
+
accountId: config.accountId,
|
|
1138
|
+
organizationId: config.organizationId,
|
|
1139
|
+
environment: config.environment,
|
|
1140
|
+
});
|
|
1141
|
+
const probeKey = loadAgentSettingsApiKey();
|
|
1142
|
+
if (probeKey)
|
|
1143
|
+
probeClient.setAgentSettingsApiKey(probeKey);
|
|
1144
|
+
const candidates = [];
|
|
1145
|
+
try {
|
|
1146
|
+
let probePage = 1;
|
|
1147
|
+
let probeHasMore = true;
|
|
1148
|
+
while (probeHasMore) {
|
|
1149
|
+
const resp = await probeClient.agentManagement.listAgents(probeClient.getOrganizationId(), probePage, 50, { network_id: projectNetworkId });
|
|
1150
|
+
candidates.push(...resp.items);
|
|
1151
|
+
probeHasMore = probePage < resp.pages;
|
|
1152
|
+
probePage++;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
catch (probeErr) {
|
|
1156
|
+
if (process.env.AUI_DEBUG) {
|
|
1157
|
+
console.warn(`[debug] resolveVersionDraft multi-agent fallback listAgents failed:`, probeErr instanceof Error ? probeErr.message : probeErr);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const siblings = candidates.filter((c) => c.id !== agentInfo.id);
|
|
1161
|
+
if (process.env.AUI_DEBUG) {
|
|
1162
|
+
console.log(`[debug] resolveVersionDraft: probing ${siblings.length} sibling agent(s) for version ${explicitVersionId}`);
|
|
1163
|
+
}
|
|
1164
|
+
for (const sibling of siblings) {
|
|
1165
|
+
try {
|
|
1166
|
+
ver = await probeClient.agentManagement.getVersion(sibling.id, explicitVersionId);
|
|
1167
|
+
owningAgent = sibling;
|
|
1168
|
+
if (process.env.AUI_DEBUG) {
|
|
1169
|
+
console.log(`[debug] resolveVersionDraft: version ${explicitVersionId} found on sibling agent ${sibling.id}`);
|
|
1170
|
+
}
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
catch (siblingErr) {
|
|
1174
|
+
const sStatus = siblingErr
|
|
1175
|
+
.statusCode ??
|
|
1176
|
+
siblingErr.status;
|
|
1177
|
+
if (sStatus !== 404)
|
|
1178
|
+
throw siblingErr;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (!ver) {
|
|
1183
|
+
throw new CLIError(`Could not fetch version "${explicitVersionId}" on any agent record for this network.`, {
|
|
1184
|
+
suggestion: "Verify the version ID with `aui version list` or from the Playground's `Versions management` dialog. The version may belong to a different account / organization scope.",
|
|
1185
|
+
cause: error,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1198
1188
|
}
|
|
1199
1189
|
if (ver.status !== "draft") {
|
|
1200
1190
|
throw new ValidationError(`Version v${ver.version_number} is "${ver.status}" — you can only push to a draft version.`, {
|
|
@@ -1202,9 +1192,14 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
|
|
|
1202
1192
|
});
|
|
1203
1193
|
}
|
|
1204
1194
|
const label = `v${ver.version_number}`;
|
|
1205
|
-
return {
|
|
1195
|
+
return {
|
|
1196
|
+
versionId: ver.id,
|
|
1197
|
+
label,
|
|
1198
|
+
agentId: owningAgent.id,
|
|
1199
|
+
agentInfo: owningAgent,
|
|
1200
|
+
};
|
|
1206
1201
|
}
|
|
1207
|
-
// Resolve from .auirc version_id or auto-detect drafts
|
|
1202
|
+
// Resolve from .auirc version_id or auto-detect drafts.
|
|
1208
1203
|
let allVersions = [];
|
|
1209
1204
|
try {
|
|
1210
1205
|
const versionsResp = await client.agentManagement.listVersions(agentInfo.id, 1, 50);
|
|
@@ -1216,7 +1211,6 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
|
|
|
1216
1211
|
cause: error,
|
|
1217
1212
|
});
|
|
1218
1213
|
}
|
|
1219
|
-
// If .auirc has a version_id, validate it's still a draft
|
|
1220
1214
|
if (projectConfig.version_id) {
|
|
1221
1215
|
const configVersion = allVersions.find((v) => v.id === projectConfig.version_id);
|
|
1222
1216
|
if (configVersion) {
|
|
@@ -1226,10 +1220,15 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
|
|
|
1226
1220
|
});
|
|
1227
1221
|
}
|
|
1228
1222
|
const label = `v${configVersion.version_number}`;
|
|
1229
|
-
return {
|
|
1223
|
+
return {
|
|
1224
|
+
versionId: configVersion.id,
|
|
1225
|
+
label,
|
|
1226
|
+
agentId: agentInfo.id,
|
|
1227
|
+
agentInfo,
|
|
1228
|
+
};
|
|
1230
1229
|
}
|
|
1231
1230
|
}
|
|
1232
|
-
// Auto-detect drafts
|
|
1231
|
+
// Auto-detect drafts.
|
|
1233
1232
|
const drafts = allVersions.filter((v) => v.status === "draft");
|
|
1234
1233
|
if (drafts.length === 0) {
|
|
1235
1234
|
throw new ValidationError("No draft version found — you can only push to a draft version.", {
|
|
@@ -1239,139 +1238,27 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
|
|
|
1239
1238
|
if (drafts.length === 1) {
|
|
1240
1239
|
const draft = drafts[0];
|
|
1241
1240
|
const label = `v${draft.version_number}`;
|
|
1242
|
-
return {
|
|
1241
|
+
return {
|
|
1242
|
+
versionId: draft.id,
|
|
1243
|
+
label,
|
|
1244
|
+
agentId: agentInfo.id,
|
|
1245
|
+
agentInfo,
|
|
1246
|
+
};
|
|
1243
1247
|
}
|
|
1244
|
-
// Multiple drafts — auto-pick the latest
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1248
|
+
// Multiple drafts — auto-pick the latest. No interactive prompt
|
|
1249
|
+
// (keeps `aui push` non-blocking). Use --version-id to target a
|
|
1250
|
+
// different draft.
|
|
1247
1251
|
const latest = drafts[0];
|
|
1248
1252
|
const latestLabel = `v${latest.version_number}`;
|
|
1249
1253
|
if (!isJsonMode()) {
|
|
1250
1254
|
log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "info", label: `${drafts.length} drafts found — auto-selecting latest: ${latestLabel}` }), _jsxs(Text, { color: colors.muted, children: [" Tip: pass ", _jsxs(Text, { bold: true, children: ["--version-id ", '<id>'] }), " to target a specific draft."] })] }));
|
|
1251
1255
|
}
|
|
1252
|
-
return { versionId: latest.id, label: latestLabel, agentId: agentInfo.id };
|
|
1253
|
-
}
|
|
1254
|
-
/**
|
|
1255
|
-
* Push a snapshot of all local agent files to the server via multipart upload.
|
|
1256
|
-
* This captures the current state of the files to maintain version history.
|
|
1257
|
-
* Version bump happens automatically on the server when snapshot succeeds.
|
|
1258
|
-
*
|
|
1259
|
-
* If any files fail, the user must re-run `aui push` to retry.
|
|
1260
|
-
*/
|
|
1261
|
-
async function pushSnapshot(client, agentId, versionId, projectRoot, fileData) {
|
|
1262
|
-
const filesToUpload = [];
|
|
1263
|
-
for (const fd of fileData) {
|
|
1264
|
-
const fullPath = path.join(projectRoot, fd.file);
|
|
1265
|
-
if (!fs.existsSync(fullPath))
|
|
1266
|
-
continue;
|
|
1267
|
-
const content = fs.readFileSync(fullPath);
|
|
1268
|
-
filesToUpload.push({
|
|
1269
|
-
filePath: fullPath,
|
|
1270
|
-
fileName: path.basename(fd.file),
|
|
1271
|
-
content,
|
|
1272
|
-
});
|
|
1273
|
-
}
|
|
1274
|
-
if (filesToUpload.length === 0) {
|
|
1275
|
-
return { success: true, failed: [] };
|
|
1276
|
-
}
|
|
1277
|
-
const result = await client.agentManagement.pushSnapshot(agentId, versionId, filesToUpload);
|
|
1278
1256
|
return {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}
|
|
1284
|
-
// ─── Agent Settings Params Resolution ───
|
|
1285
|
-
async function resolveAgentSettingsParams(config, projectConfig, session, projectRoot, scopeLevel) {
|
|
1286
|
-
// Throws ConfigError on any missing config field — never returns null.
|
|
1287
|
-
// See note on resolveVersionDraft for why silent returns are forbidden.
|
|
1288
|
-
const networkId = projectConfig.agent_id || session.network_id;
|
|
1289
|
-
const accountId = projectConfig.account_id || config.accountId;
|
|
1290
|
-
const organizationId = projectConfig.organization_id || config.organizationId;
|
|
1291
|
-
const userId = session.user_id;
|
|
1292
|
-
if (!networkId || !accountId || !organizationId) {
|
|
1293
|
-
const missing = [];
|
|
1294
|
-
if (!networkId)
|
|
1295
|
-
missing.push("network_id (agent_id in .auirc)");
|
|
1296
|
-
if (!accountId)
|
|
1297
|
-
missing.push("account_id (in .auirc or session)");
|
|
1298
|
-
if (!organizationId)
|
|
1299
|
-
missing.push("organization_id (in .auirc or session)");
|
|
1300
|
-
throw new ConfigError(`Missing: ${missing.join(", ")}`, {
|
|
1301
|
-
suggestion: "Re-import the agent (`aui import`) or re-login (`aui login`).",
|
|
1302
|
-
});
|
|
1303
|
-
}
|
|
1304
|
-
let categoryId = projectConfig.network_category_id;
|
|
1305
|
-
if (!categoryId) {
|
|
1306
|
-
try {
|
|
1307
|
-
const client = new AUIClient({
|
|
1308
|
-
baseUrl: config.apiUrl,
|
|
1309
|
-
authToken: config.authToken,
|
|
1310
|
-
accountId,
|
|
1311
|
-
organizationId,
|
|
1312
|
-
environment: config.environment,
|
|
1313
|
-
});
|
|
1314
|
-
const resp = (await client.networks.get(networkId));
|
|
1315
|
-
const cat = resp?.data?.category;
|
|
1316
|
-
if (cat) {
|
|
1317
|
-
categoryId = typeof cat === "string" ? cat : cat._id;
|
|
1318
|
-
saveProjectConfig({ ...projectConfig, network_category_id: categoryId }, projectRoot);
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
catch (err) {
|
|
1322
|
-
// Falls through to the explicit ConfigError below if no categoryId
|
|
1323
|
-
// could be resolved. Surface in AUI_DEBUG so the operator can see
|
|
1324
|
-
// why the auto-fetch failed instead of just the generic "Missing
|
|
1325
|
-
// network_category_id" message.
|
|
1326
|
-
if (process.env.AUI_DEBUG) {
|
|
1327
|
-
console.warn(`[debug] resolveAgentSettingsParams: networks.get(${networkId}) failed:`, err instanceof Error ? err.message : err);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
if (!categoryId) {
|
|
1332
|
-
throw new ConfigError("Missing network_category_id.", {
|
|
1333
|
-
suggestion: "Re-import the agent (`aui import`) to fix.",
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1336
|
-
const baseParams = {
|
|
1337
|
-
updated_by: userId,
|
|
1338
|
-
network_id: networkId,
|
|
1339
|
-
account_id: accountId,
|
|
1340
|
-
organization_id: organizationId,
|
|
1341
|
-
network_category_id: categoryId,
|
|
1257
|
+
versionId: latest.id,
|
|
1258
|
+
label: latestLabel,
|
|
1259
|
+
agentId: agentInfo.id,
|
|
1260
|
+
agentInfo,
|
|
1342
1261
|
};
|
|
1343
|
-
if (scopeLevel && scopeLevel !== "network") {
|
|
1344
|
-
const scoped = applyScopeLevel({
|
|
1345
|
-
network_id: networkId,
|
|
1346
|
-
account_id: accountId,
|
|
1347
|
-
organization_id: organizationId,
|
|
1348
|
-
network_category_id: categoryId,
|
|
1349
|
-
}, scopeLevel);
|
|
1350
|
-
if (process.env.AUI_DEBUG) {
|
|
1351
|
-
console.log(`[debug] push scope params: ${JSON.stringify(scoped)}`);
|
|
1352
|
-
}
|
|
1353
|
-
return {
|
|
1354
|
-
updated_by: userId,
|
|
1355
|
-
network_id: scoped.network_id ?? "",
|
|
1356
|
-
account_id: scoped.account_id ?? "",
|
|
1357
|
-
organization_id: scoped.organization_id ?? "",
|
|
1358
|
-
network_category_id: scoped.network_category_id ?? "",
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
|
-
return baseParams;
|
|
1362
|
-
}
|
|
1363
|
-
// ─── Push Task Classification Helpers ───
|
|
1364
|
-
/**
|
|
1365
|
-
* Integration tasks split into two phases by the unified push order:
|
|
1366
|
-
* - Upserts (POST/PATCH/PUT) run BEFORE knowledge bases + tools, so KBs
|
|
1367
|
-
* can attach to integrations and tools can reference integration codes.
|
|
1368
|
-
* - Deletes run AFTER tools / entities, so the last write that mentioned
|
|
1369
|
-
* the integration succeeds before the row is removed.
|
|
1370
|
-
*/
|
|
1371
|
-
function isIntegrationUpsertTask(t) {
|
|
1372
|
-
return (t.type === "put-integrations" ||
|
|
1373
|
-
t.type === "create-integration" ||
|
|
1374
|
-
t.type === "patch-integration");
|
|
1375
1262
|
}
|
|
1376
1263
|
async function pushKnowledgeHubs(projectRoot, projectConfig) {
|
|
1377
1264
|
const { getKnowledgeHubChanges } = await import("../utils/git.js");
|
|
@@ -1433,7 +1320,7 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
|
|
|
1433
1320
|
}
|
|
1434
1321
|
// Per-KB delete in its own span so each one shows up in Logfire as
|
|
1435
1322
|
// `aui.push.task.kb-delete` with status, kb name, kb id, and error
|
|
1436
|
-
// body
|
|
1323
|
+
// body — same observability shape as the previous per-entity tasks.
|
|
1437
1324
|
const kbDelTracer = getTracer();
|
|
1438
1325
|
await kbDelTracer.startActiveSpan("aui.push.task.kb-delete", async (span) => {
|
|
1439
1326
|
span.setAttribute("push.task.type", "kb-delete");
|
|
@@ -1459,7 +1346,10 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
|
|
|
1459
1346
|
}
|
|
1460
1347
|
catch (delErr) {
|
|
1461
1348
|
// Per-KB error: count it, keep going so partial work shows up.
|
|
1462
|
-
|
|
1349
|
+
// 404 → already absent (success); other errors → real failure.
|
|
1350
|
+
const statusCode = delErr
|
|
1351
|
+
.statusCode ?? delErr.status;
|
|
1352
|
+
if (statusCode === 404) {
|
|
1463
1353
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
1464
1354
|
span.addEvent("fallback.delete_404_already_absent");
|
|
1465
1355
|
span.setAttribute("push.task.fallback", "delete_404_already_absent");
|
|
@@ -1471,8 +1361,8 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
|
|
|
1471
1361
|
span.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
1472
1362
|
span.recordException(delErr instanceof Error ? delErr : new Error(errMsg));
|
|
1473
1363
|
span.setAttribute("push.task.error", errMsg);
|
|
1474
|
-
if (
|
|
1475
|
-
span.setAttribute("push.task.error_status_code",
|
|
1364
|
+
if (statusCode) {
|
|
1365
|
+
span.setAttribute("push.task.error_status_code", statusCode);
|
|
1476
1366
|
}
|
|
1477
1367
|
failures.push({
|
|
1478
1368
|
label: `Delete knowledge base: ${kbName}`,
|
|
@@ -1522,9 +1412,6 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
|
|
|
1522
1412
|
log(_jsx(Box, { paddingX: 1, children: _jsx(StatusLine, { kind: "warning", label: `Skipped unsupported file: ${path.basename(skipped)} (only .pdf, .md, .txt, .json)` }) }));
|
|
1523
1413
|
}
|
|
1524
1414
|
if (supportedFiles.length > 0) {
|
|
1525
|
-
// Per-KB upload in its own span — Logfire query
|
|
1526
|
-
// `name:"aui.push.task.kb-upload" AND status_code:ERROR` finds
|
|
1527
|
-
// every KB push failure across all agents.
|
|
1528
1415
|
const kbUpTracer = getTracer();
|
|
1529
1416
|
await kbUpTracer.startActiveSpan("aui.push.task.kb-upload", async (span) => {
|
|
1530
1417
|
span.setAttribute("push.task.type", "kb-upload");
|
|
@@ -1561,8 +1448,6 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
|
|
|
1561
1448
|
fs.writeFileSync(kbJsonPath, JSON.stringify(raw, null, 2) + "\n");
|
|
1562
1449
|
}
|
|
1563
1450
|
catch (writeErr) {
|
|
1564
|
-
// kb.json id write fail is non-fatal but tell the user so the
|
|
1565
|
-
// next push doesn't surprise them with "no knowledge_base_id stored".
|
|
1566
1451
|
span.addEvent("kb_id_writeback_failed");
|
|
1567
1452
|
if (process.env.AUI_DEBUG) {
|
|
1568
1453
|
console.warn(`[debug] failed to write knowledge_base_id back to ${kbJsonPath}:`, writeErr);
|
|
@@ -1573,9 +1458,13 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
|
|
|
1573
1458
|
}
|
|
1574
1459
|
catch (uploadErr) {
|
|
1575
1460
|
hadUploadFailure = true;
|
|
1576
|
-
const errMsg = uploadErr instanceof Error
|
|
1461
|
+
const errMsg = uploadErr instanceof Error
|
|
1462
|
+
? uploadErr.message
|
|
1463
|
+
: String(uploadErr);
|
|
1577
1464
|
span.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
1578
|
-
span.recordException(uploadErr instanceof Error
|
|
1465
|
+
span.recordException(uploadErr instanceof Error
|
|
1466
|
+
? uploadErr
|
|
1467
|
+
: new Error(errMsg));
|
|
1579
1468
|
span.setAttribute("push.task.error", errMsg);
|
|
1580
1469
|
if (uploadErr.statusCode) {
|
|
1581
1470
|
span.setAttribute("push.task.error_status_code", uploadErr.statusCode);
|
|
@@ -1701,8 +1590,43 @@ async function pushKnowledgeHubs(projectRoot, projectConfig) {
|
|
|
1701
1590
|
}
|
|
1702
1591
|
return { ok: kbPushSucceeded && failures.length === 0, failures };
|
|
1703
1592
|
}
|
|
1704
|
-
// ───
|
|
1593
|
+
// ─── Diff helper: read array metadata from a known top-level shape ───
|
|
1705
1594
|
function getArrayFileInfoForPush(filePath, dir) {
|
|
1595
|
+
// Filename-first detection (same rationale as the classifier in
|
|
1596
|
+
// `_push`'s read loop) — under the new /pull endpoint the canonical
|
|
1597
|
+
// files may or may not be wrapped depending on push origin, but
|
|
1598
|
+
// their filenames are stable. `agent.aui.json` is intentionally
|
|
1599
|
+
// NOT in this map: it's a single general_settings object, not an
|
|
1600
|
+
// array file, so item-level diff doesn't apply.
|
|
1601
|
+
const basename = path.basename(filePath);
|
|
1602
|
+
const FILENAME_ARRAY_INFO = {
|
|
1603
|
+
"parameters.aui.json": {
|
|
1604
|
+
arrayKey: "parameters",
|
|
1605
|
+
itemKey: "code",
|
|
1606
|
+
label: "Parameters",
|
|
1607
|
+
},
|
|
1608
|
+
"entities.aui.json": {
|
|
1609
|
+
arrayKey: "entities",
|
|
1610
|
+
itemKey: "name",
|
|
1611
|
+
label: "Entities",
|
|
1612
|
+
},
|
|
1613
|
+
"integrations.aui.json": {
|
|
1614
|
+
arrayKey: "integrations",
|
|
1615
|
+
itemKey: "code",
|
|
1616
|
+
label: "Integrations",
|
|
1617
|
+
},
|
|
1618
|
+
"rules.aui.json": {
|
|
1619
|
+
arrayKey: "rules",
|
|
1620
|
+
itemKey: "code",
|
|
1621
|
+
label: "Rules",
|
|
1622
|
+
},
|
|
1623
|
+
};
|
|
1624
|
+
if (basename in FILENAME_ARRAY_INFO) {
|
|
1625
|
+
return FILENAME_ARRAY_INFO[basename];
|
|
1626
|
+
}
|
|
1627
|
+
// Fallback to content-based detection for non-canonical filenames
|
|
1628
|
+
// (legacy projects, custom layouts). Tool files are NOT array
|
|
1629
|
+
// files — return null so the per-tool diff stays at file-level.
|
|
1706
1630
|
try {
|
|
1707
1631
|
const fullPath = path.join(dir, filePath);
|
|
1708
1632
|
if (!fs.existsSync(fullPath))
|
|
@@ -1729,7 +1653,7 @@ function getArrayFileInfoForPush(filePath, dir) {
|
|
|
1729
1653
|
return null;
|
|
1730
1654
|
}
|
|
1731
1655
|
}
|
|
1732
|
-
function writePushMemory(projectRoot,
|
|
1656
|
+
function writePushMemory(projectRoot, input) {
|
|
1733
1657
|
try {
|
|
1734
1658
|
const memoryDir = path.join(projectRoot, "memory");
|
|
1735
1659
|
fs.mkdirSync(memoryDir, { recursive: true });
|
|
@@ -1737,80 +1661,52 @@ function writePushMemory(projectRoot, agentCode, agentId, pushTasks, succeededFi
|
|
|
1737
1661
|
const timestamp = now.toISOString().replace(/[:.]/g, "-");
|
|
1738
1662
|
const dateStr = now.toISOString().replace("T", " ").substring(0, 19);
|
|
1739
1663
|
const filename = `push-${timestamp}.md`;
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const totalSucceeded = pushTasks.filter((t) => (t.file && succeededSet.has(t.file)) ||
|
|
1746
|
-
(!t.file && !failureMap.has(t.label))).length;
|
|
1747
|
-
const totalFailed = pushFailures.length;
|
|
1748
|
-
const overallStatus = totalFailed === 0 ? "SUCCESS" : totalSucceeded > 0 ? "PARTIAL" : "FAILED";
|
|
1664
|
+
const overallStatus = input.pushSucceeded
|
|
1665
|
+
? input.kbFailures.length === 0
|
|
1666
|
+
? "SUCCESS"
|
|
1667
|
+
: "PARTIAL"
|
|
1668
|
+
: "FAILED";
|
|
1749
1669
|
const lines = [
|
|
1750
1670
|
`# Push ${dateStr}`,
|
|
1751
1671
|
"",
|
|
1752
|
-
`**Agent:** ${agentCode}`,
|
|
1753
|
-
`**Agent ID:** ${agentId}`,
|
|
1672
|
+
`**Agent:** ${input.agentCode}`,
|
|
1673
|
+
`**Agent ID:** ${input.agentId}`,
|
|
1674
|
+
`**Draft:** ${input.versionLabel}`,
|
|
1675
|
+
`**New tag:** ${input.newVersionTag ?? "(none — push did not succeed)"}`,
|
|
1676
|
+
`**Revision ID:** ${input.revisionId ?? "(none)"}`,
|
|
1754
1677
|
`**Status:** ${overallStatus}`,
|
|
1755
|
-
`**
|
|
1756
|
-
"",
|
|
1757
|
-
"## Changes",
|
|
1678
|
+
`**Bundle:** ${input.uploadedFiles.length} file(s) uploaded`,
|
|
1758
1679
|
"",
|
|
1759
1680
|
];
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
if (!tasksByCategory[category])
|
|
1764
|
-
tasksByCategory[category] = [];
|
|
1765
|
-
tasksByCategory[category].push(task);
|
|
1681
|
+
if (input.commitMessage) {
|
|
1682
|
+
lines.push(`**Commit message:** ${input.commitMessage}`);
|
|
1683
|
+
lines.push("");
|
|
1766
1684
|
}
|
|
1767
|
-
|
|
1768
|
-
lines.push(
|
|
1685
|
+
if (!input.pushSucceeded && input.bundleError) {
|
|
1686
|
+
lines.push("## Bundle Error", "");
|
|
1687
|
+
lines.push("```");
|
|
1688
|
+
lines.push(input.bundleError);
|
|
1689
|
+
lines.push("```", "");
|
|
1690
|
+
}
|
|
1691
|
+
lines.push("## Files in bundle", "");
|
|
1692
|
+
for (const fd of input.fileData) {
|
|
1693
|
+
lines.push(`- \`${fd.file}\` _(${fd.type})_`);
|
|
1694
|
+
}
|
|
1695
|
+
lines.push("");
|
|
1696
|
+
if (input.diff && input.diff.changedFiles.length > 0) {
|
|
1697
|
+
lines.push("## Changed since last push", "");
|
|
1698
|
+
for (const change of input.diff.changedFiles) {
|
|
1699
|
+
lines.push(`- **${change.status}** \`${change.file}\``);
|
|
1700
|
+
}
|
|
1769
1701
|
lines.push("");
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
lines.push(
|
|
1776
|
-
if (
|
|
1777
|
-
lines.push(` - File: \`${
|
|
1778
|
-
}
|
|
1779
|
-
if (isFailed) {
|
|
1780
|
-
lines.push(` - Error: ${failureMap.get(task.label)}`);
|
|
1781
|
-
}
|
|
1782
|
-
if (action === "delete") {
|
|
1783
|
-
lines.push(` - Action: Deleted`);
|
|
1784
|
-
}
|
|
1785
|
-
else if (action === "create" || !task.oldBody) {
|
|
1786
|
-
lines.push(` - Action: Created`);
|
|
1787
|
-
lines.push(` - Body:`);
|
|
1788
|
-
lines.push(" ```json");
|
|
1789
|
-
lines.push(` ${JSON.stringify(task.body, null, 2).split("\n").join("\n ")}`);
|
|
1790
|
-
lines.push(" ```");
|
|
1791
|
-
}
|
|
1792
|
-
else {
|
|
1793
|
-
const fieldDiffs = diffObjects(task.oldBody, task.body);
|
|
1794
|
-
if (fieldDiffs.length > 0) {
|
|
1795
|
-
lines.push(` - Action: Modified`);
|
|
1796
|
-
lines.push(` - Diff:`);
|
|
1797
|
-
for (const fd of fieldDiffs) {
|
|
1798
|
-
if (fd.type === "added") {
|
|
1799
|
-
lines.push(` - **+ ${fd.path}**: \`${truncate(fd.newValue)}\``);
|
|
1800
|
-
}
|
|
1801
|
-
else if (fd.type === "removed") {
|
|
1802
|
-
lines.push(` - **- ${fd.path}**: ~~\`${truncate(fd.oldValue)}\`~~`);
|
|
1803
|
-
}
|
|
1804
|
-
else {
|
|
1805
|
-
lines.push(` - **~ ${fd.path}**: \`${truncate(fd.oldValue)}\` → \`${truncate(fd.newValue)}\``);
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
else {
|
|
1810
|
-
lines.push(` - Action: Modified (no field-level diff detected)`);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
lines.push("");
|
|
1702
|
+
}
|
|
1703
|
+
if (input.kbFailures.length > 0) {
|
|
1704
|
+
lines.push("## Knowledge-base failures", "");
|
|
1705
|
+
for (const f of input.kbFailures) {
|
|
1706
|
+
lines.push(`- :x: **${f.label}**`);
|
|
1707
|
+
lines.push(` - Error: ${f.error}`);
|
|
1708
|
+
if (f.file)
|
|
1709
|
+
lines.push(` - File: \`${f.file}\``);
|
|
1814
1710
|
}
|
|
1815
1711
|
lines.push("");
|
|
1816
1712
|
}
|
|
@@ -1820,673 +1716,10 @@ function writePushMemory(projectRoot, agentCode, agentId, pushTasks, succeededFi
|
|
|
1820
1716
|
}
|
|
1821
1717
|
catch (err) {
|
|
1822
1718
|
// Memory file is diagnostic only — its failure shouldn't block the push.
|
|
1823
|
-
// But emit a debug warning so an operator chasing "where's my push memory"
|
|
1824
|
-
// sees what went wrong.
|
|
1825
1719
|
if (process.env.AUI_DEBUG) {
|
|
1826
1720
|
console.warn("[debug] writePushMemory failed:", err instanceof Error ? err.message : err);
|
|
1827
1721
|
}
|
|
1828
1722
|
return undefined;
|
|
1829
1723
|
}
|
|
1830
1724
|
}
|
|
1831
|
-
function truncate(value, maxLen = 120) {
|
|
1832
|
-
const str = typeof value === "string" ? value : JSON.stringify(value);
|
|
1833
|
-
if (!str)
|
|
1834
|
-
return "(empty)";
|
|
1835
|
-
return str.length > maxLen ? str.substring(0, maxLen) + "..." : str;
|
|
1836
|
-
}
|
|
1837
|
-
function diffObjects(oldObj, newObj, prefix = "") {
|
|
1838
|
-
const diffs = [];
|
|
1839
|
-
const allKeys = new Set([
|
|
1840
|
-
...Object.keys(oldObj || {}),
|
|
1841
|
-
...Object.keys(newObj || {}),
|
|
1842
|
-
]);
|
|
1843
|
-
for (const key of Array.from(allKeys)) {
|
|
1844
|
-
const p = prefix ? `${prefix}.${key}` : key;
|
|
1845
|
-
const oldVal = oldObj?.[key];
|
|
1846
|
-
const newVal = newObj?.[key];
|
|
1847
|
-
if (oldVal === undefined && newVal !== undefined) {
|
|
1848
|
-
diffs.push({ path: p, type: "added", newValue: newVal });
|
|
1849
|
-
}
|
|
1850
|
-
else if (oldVal !== undefined && newVal === undefined) {
|
|
1851
|
-
diffs.push({ path: p, type: "removed", oldValue: oldVal });
|
|
1852
|
-
}
|
|
1853
|
-
else if (oldVal !== null &&
|
|
1854
|
-
newVal !== null &&
|
|
1855
|
-
typeof oldVal === "object" &&
|
|
1856
|
-
typeof newVal === "object" &&
|
|
1857
|
-
!Array.isArray(oldVal) &&
|
|
1858
|
-
!Array.isArray(newVal)) {
|
|
1859
|
-
diffs.push(...diffObjects(oldVal, newVal, p));
|
|
1860
|
-
}
|
|
1861
|
-
else if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
1862
|
-
diffs.push({
|
|
1863
|
-
path: p,
|
|
1864
|
-
type: "changed",
|
|
1865
|
-
oldValue: oldVal,
|
|
1866
|
-
newValue: newVal,
|
|
1867
|
-
});
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
return diffs;
|
|
1871
|
-
}
|
|
1872
|
-
/**
|
|
1873
|
-
* Build the ordered list of API tasks the push will execute, given a git
|
|
1874
|
-
* diff summary, the parsed file contents, and an injectable per-file diff
|
|
1875
|
-
* function.
|
|
1876
|
-
*
|
|
1877
|
-
* `getFileDiffFn` is parameterized so tests can stub out `git show` without
|
|
1878
|
-
* a real git repo. `getItemLevelDiffFn` is optional and defaults to the
|
|
1879
|
-
* production implementation; tests inject it so the array-keyed branches
|
|
1880
|
-
* (parameters, entities, integrations) are exercisable without git too.
|
|
1881
|
-
*
|
|
1882
|
-
* Exported for unit-test access only — production callers should keep
|
|
1883
|
-
* going through `_push`, which threads in the real `getFileDiff` and
|
|
1884
|
-
* `getItemLevelDiff` from `../utils/git.js`.
|
|
1885
|
-
*/
|
|
1886
|
-
export function buildPushTasks(diff, fileData, projectRoot, getFileDiffFn, getItemLevelDiffFn) {
|
|
1887
|
-
const getItems = getItemLevelDiffFn ?? getItemLevelDiff;
|
|
1888
|
-
const tasks = [];
|
|
1889
|
-
for (const change of diff.changedFiles) {
|
|
1890
|
-
if (change.status === "deleted") {
|
|
1891
|
-
const isToolFile = change.file.includes("tools/") && change.file.endsWith(".aui.json");
|
|
1892
|
-
if (isToolFile) {
|
|
1893
|
-
const basename = change.file.split("/").pop().replace(".aui.json", "");
|
|
1894
|
-
const toolName = basename.toUpperCase().replace(/-/g, "_");
|
|
1895
|
-
tasks.push({
|
|
1896
|
-
type: "delete-tool",
|
|
1897
|
-
label: `Delete tool: ${toolName}`,
|
|
1898
|
-
toolName,
|
|
1899
|
-
file: change.file,
|
|
1900
|
-
body: {},
|
|
1901
|
-
});
|
|
1902
|
-
}
|
|
1903
|
-
continue;
|
|
1904
|
-
}
|
|
1905
|
-
const fd = fileData.find((f) => f.file === change.file ||
|
|
1906
|
-
change.file.endsWith("/" + f.file) ||
|
|
1907
|
-
f.file.endsWith("/" + change.file));
|
|
1908
|
-
if (!fd)
|
|
1909
|
-
continue;
|
|
1910
|
-
if (fd.type === "tool") {
|
|
1911
|
-
const toolData = fd.data?.tool || fd.data;
|
|
1912
|
-
if (!toolData || !toolData.code)
|
|
1913
|
-
continue;
|
|
1914
|
-
const toolCode = toolData.code || "";
|
|
1915
|
-
const toolName = toolCode.toUpperCase().replace(/-/g, "_");
|
|
1916
|
-
if (change.status === "added") {
|
|
1917
|
-
tasks.push({
|
|
1918
|
-
type: "create-tool",
|
|
1919
|
-
label: `Create tool: ${toolCode}`,
|
|
1920
|
-
file: change.file,
|
|
1921
|
-
body: toolData,
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
else if (change.status === "modified") {
|
|
1925
|
-
// `change.status === "modified"` comes from `getDiffSummary` (git
|
|
1926
|
-
// status). That alone is the source of truth that the file content
|
|
1927
|
-
// diverged from the baseline; always push the patch. The full
|
|
1928
|
-
// `toolData` body is sent (consistent with the create-tool branch
|
|
1929
|
-
// immediately above); the platform PATCH is idempotent for fields
|
|
1930
|
-
// whose value didn't actually change, so this is safe.
|
|
1931
|
-
//
|
|
1932
|
-
// Pre-fix this branch routed the changes through a
|
|
1933
|
-
// `buildToolPatchBody` gate that only matched diff paths starting
|
|
1934
|
-
// with the wrapped-shape `tool.` prefix. Tool files written in flat
|
|
1935
|
-
// shape (no `{ "tool": {...} }` wrapper — accepted by the loader at
|
|
1936
|
-
// ~L2675 and by `aui validate`) diffed to top-level paths
|
|
1937
|
-
// (`response_type`, `widget`, `goal`, …), the gate returned an
|
|
1938
|
-
// empty body, and the `patch-tool` task was silently dropped.
|
|
1939
|
-
// Symptom users hit: `aui diff` showed the change, `aui push`
|
|
1940
|
-
// printed "No pushable changes detected" and exited 0.
|
|
1941
|
-
tasks.push({
|
|
1942
|
-
type: "patch-tool",
|
|
1943
|
-
label: `Update tool: ${toolCode}`,
|
|
1944
|
-
toolName,
|
|
1945
|
-
file: change.file,
|
|
1946
|
-
body: toolData,
|
|
1947
|
-
});
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
else if (fd.type === "general_settings") {
|
|
1951
|
-
if (change.status === "modified") {
|
|
1952
|
-
const fieldDiffs = getFileDiffFn(projectRoot, change.file);
|
|
1953
|
-
const patchBody = buildSettingsPatchBody(fieldDiffs);
|
|
1954
|
-
if (Object.keys(patchBody).length > 0) {
|
|
1955
|
-
tasks.push({
|
|
1956
|
-
type: "patch-general-settings",
|
|
1957
|
-
label: "Update general settings",
|
|
1958
|
-
file: change.file,
|
|
1959
|
-
body: patchBody,
|
|
1960
|
-
});
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
else if (change.status === "added") {
|
|
1964
|
-
// Mirror the tool loader's `(fd.data as any)?.tool || fd.data`
|
|
1965
|
-
// tolerance: accept both `{ "general_settings": {...} }` (wrapped)
|
|
1966
|
-
// and `{...}` (flat). `normalizeGeneralSettings` in
|
|
1967
|
-
// `pull-agent.tsx` / `import-agent.tsx` and `aui validate`
|
|
1968
|
-
// (`commands/validate.tsx:222`) already permit both layouts —
|
|
1969
|
-
// staying in lockstep with them here prevents the same
|
|
1970
|
-
// "silently dropped" class of bug the tool-modify branch had.
|
|
1971
|
-
const settings = fd.data?.general_settings ?? fd.data;
|
|
1972
|
-
if (settings) {
|
|
1973
|
-
tasks.push({
|
|
1974
|
-
type: "patch-general-settings",
|
|
1975
|
-
label: "Update general settings",
|
|
1976
|
-
file: change.file,
|
|
1977
|
-
body: settings,
|
|
1978
|
-
});
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
else if (fd.type === "parameters") {
|
|
1983
|
-
const itemDiffs = getItems(projectRoot, change.file, "parameters", "code");
|
|
1984
|
-
for (const item of itemDiffs) {
|
|
1985
|
-
if (item.status === "added") {
|
|
1986
|
-
tasks.push({
|
|
1987
|
-
type: "create-parameter",
|
|
1988
|
-
label: `Create parameter: ${item.key}`,
|
|
1989
|
-
file: change.file,
|
|
1990
|
-
body: item.newItem,
|
|
1991
|
-
itemCode: item.key,
|
|
1992
|
-
});
|
|
1993
|
-
}
|
|
1994
|
-
else if (item.status === "modified") {
|
|
1995
|
-
tasks.push({
|
|
1996
|
-
type: "patch-parameter",
|
|
1997
|
-
label: `Update parameter: ${item.key}`,
|
|
1998
|
-
file: change.file,
|
|
1999
|
-
body: item.newItem,
|
|
2000
|
-
itemCode: item.key,
|
|
2001
|
-
});
|
|
2002
|
-
}
|
|
2003
|
-
else if (item.status === "deleted") {
|
|
2004
|
-
tasks.push({
|
|
2005
|
-
type: "delete-parameter",
|
|
2006
|
-
label: `Delete parameter: ${item.key}`,
|
|
2007
|
-
file: change.file,
|
|
2008
|
-
body: item.oldItem,
|
|
2009
|
-
itemCode: item.key,
|
|
2010
|
-
});
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
else if (fd.type === "entities") {
|
|
2015
|
-
const itemDiffs = getItems(projectRoot, change.file, "entities", "name");
|
|
2016
|
-
for (const item of itemDiffs) {
|
|
2017
|
-
if (item.status === "added") {
|
|
2018
|
-
tasks.push({
|
|
2019
|
-
type: "create-entity",
|
|
2020
|
-
label: `Create entity: ${item.key}`,
|
|
2021
|
-
file: change.file,
|
|
2022
|
-
body: item.newItem,
|
|
2023
|
-
itemCode: item.key,
|
|
2024
|
-
});
|
|
2025
|
-
}
|
|
2026
|
-
else if (item.status === "modified") {
|
|
2027
|
-
tasks.push({
|
|
2028
|
-
type: "patch-entity",
|
|
2029
|
-
label: `Update entity: ${item.key}`,
|
|
2030
|
-
file: change.file,
|
|
2031
|
-
body: item.newItem,
|
|
2032
|
-
itemCode: item.key,
|
|
2033
|
-
});
|
|
2034
|
-
}
|
|
2035
|
-
else if (item.status === "deleted") {
|
|
2036
|
-
tasks.push({
|
|
2037
|
-
type: "delete-entity",
|
|
2038
|
-
label: `Delete entity: ${item.key}`,
|
|
2039
|
-
file: change.file,
|
|
2040
|
-
body: item.oldItem,
|
|
2041
|
-
itemCode: item.key,
|
|
2042
|
-
});
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
else if (fd.type === "integrations") {
|
|
2047
|
-
const itemDiffs = getItems(projectRoot, change.file, "integrations", "code");
|
|
2048
|
-
for (const item of itemDiffs) {
|
|
2049
|
-
if (item.status === "added") {
|
|
2050
|
-
tasks.push({
|
|
2051
|
-
type: "create-integration",
|
|
2052
|
-
label: `Create integration: ${item.key}`,
|
|
2053
|
-
file: change.file,
|
|
2054
|
-
body: item.newItem,
|
|
2055
|
-
itemCode: item.key,
|
|
2056
|
-
});
|
|
2057
|
-
}
|
|
2058
|
-
else if (item.status === "modified") {
|
|
2059
|
-
tasks.push({
|
|
2060
|
-
type: "patch-integration",
|
|
2061
|
-
label: `Update integration: ${item.key}`,
|
|
2062
|
-
file: change.file,
|
|
2063
|
-
body: item.newItem,
|
|
2064
|
-
itemCode: item.key,
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
else if (item.status === "deleted") {
|
|
2068
|
-
tasks.push({
|
|
2069
|
-
type: "delete-integration",
|
|
2070
|
-
label: `Delete integration: ${item.key}`,
|
|
2071
|
-
file: change.file,
|
|
2072
|
-
body: item.oldItem,
|
|
2073
|
-
itemCode: item.key,
|
|
2074
|
-
});
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
else if (fd.type === "rules") {
|
|
2079
|
-
const rulesData = fd.data;
|
|
2080
|
-
tasks.push({
|
|
2081
|
-
type: "put-rules",
|
|
2082
|
-
label: "Update rules",
|
|
2083
|
-
file: change.file,
|
|
2084
|
-
body: rulesData,
|
|
2085
|
-
});
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return tasks;
|
|
2089
|
-
}
|
|
2090
|
-
// NOTE: `buildToolPatchBody` was removed when the tool-modify branch above
|
|
2091
|
-
// switched to pushing on any `change.status === "modified"`. Its sole job
|
|
2092
|
-
// had been to gate the patch-tool task on a `tool.<field>` diff-path prefix,
|
|
2093
|
-
// which silently dropped every edit on flat-shape (no-wrapper) tool files.
|
|
2094
|
-
// See the inline comment in the modify branch (~L2688) for the full history.
|
|
2095
|
-
export function buildSettingsPatchBody(fieldDiffs) {
|
|
2096
|
-
const body = {};
|
|
2097
|
-
for (const fd of fieldDiffs) {
|
|
2098
|
-
if (fd.operation === "removed")
|
|
2099
|
-
continue;
|
|
2100
|
-
const parts = fd.path.split(".");
|
|
2101
|
-
if (parts.length === 0 || !parts[0])
|
|
2102
|
-
continue;
|
|
2103
|
-
// general_settings (agent.aui.json) files come in two equally-supported
|
|
2104
|
-
// shapes:
|
|
2105
|
-
// wrapped: { "general_settings": { ...fields... } } → paths look like
|
|
2106
|
-
// `general_settings.<field>...`
|
|
2107
|
-
// flat: { ...fields... } → paths look like
|
|
2108
|
-
// `<field>...`
|
|
2109
|
-
// `aui validate` (`commands/validate.tsx:222`) and
|
|
2110
|
-
// `normalizeGeneralSettings` in `import-agent.tsx` / `pull-agent.tsx`
|
|
2111
|
-
// already accept both layouts. Pre-fix this body builder only matched
|
|
2112
|
-
// the wrapped form, which silently dropped edits to flat-shape files.
|
|
2113
|
-
// Match the loader behavior here so every change reaches the platform.
|
|
2114
|
-
const topLevelKey = parts[0] === "general_settings" && parts.length >= 2
|
|
2115
|
-
? parts[1]
|
|
2116
|
-
: parts[0];
|
|
2117
|
-
body[topLevelKey] = fd.newValue;
|
|
2118
|
-
}
|
|
2119
|
-
return body;
|
|
2120
|
-
}
|
|
2121
|
-
// ─── Push Task Executor ───
|
|
2122
|
-
async function executePushTask(client, params, task) {
|
|
2123
|
-
const tracer = getTracer();
|
|
2124
|
-
return tracer.startActiveSpan(`aui.push.task.${task.type}`, async (span) => {
|
|
2125
|
-
span.setAttribute("push.task.type", task.type);
|
|
2126
|
-
span.setAttribute("push.task.label", task.label);
|
|
2127
|
-
if (task.file)
|
|
2128
|
-
span.setAttribute("push.task.file", task.file);
|
|
2129
|
-
if (task.toolName)
|
|
2130
|
-
span.setAttribute("push.task.tool_name", task.toolName);
|
|
2131
|
-
if (task.itemCode)
|
|
2132
|
-
span.setAttribute("push.task.item_code", task.itemCode);
|
|
2133
|
-
// Tag every task span with who ran it and which agent/version/scope
|
|
2134
|
-
// it targeted. The parent `aui.push` span already has these, but the
|
|
2135
|
-
// Logfire row view doesn't carry parent attrs onto children — so a
|
|
2136
|
-
// 500 on `aui.push.task.patch-tool` needs them attached locally for
|
|
2137
|
-
// a triage glance to be self-sufficient. Identifiers only — no
|
|
2138
|
-
// tokens. See `setAgentContext` JSDoc for the safety rationale.
|
|
2139
|
-
await setUserContext(span);
|
|
2140
|
-
await setAgentContext(span, {
|
|
2141
|
-
agentId: params.agent_id,
|
|
2142
|
-
versionId: params.version_id,
|
|
2143
|
-
networkId: params.network_id,
|
|
2144
|
-
accountId: params.account_id,
|
|
2145
|
-
organizationId: params.organization_id,
|
|
2146
|
-
networkCategoryId: params.network_category_id,
|
|
2147
|
-
});
|
|
2148
|
-
try {
|
|
2149
|
-
span.setAttribute("push.task.request_payload", JSON.stringify(task.body));
|
|
2150
|
-
const result = await _executePushTask(client, params, task);
|
|
2151
|
-
span.setAttribute("push.task.response", JSON.stringify(result));
|
|
2152
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
2153
|
-
return result;
|
|
2154
|
-
}
|
|
2155
|
-
catch (error) {
|
|
2156
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
2157
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
2158
|
-
span.recordException(error instanceof Error ? error : new Error(errMsg));
|
|
2159
|
-
span.setAttribute("push.task.error", errMsg);
|
|
2160
|
-
if (error.statusCode)
|
|
2161
|
-
span.setAttribute("push.task.error_status_code", error.statusCode);
|
|
2162
|
-
if (error.responseBody)
|
|
2163
|
-
span.setAttribute("push.task.error_response", JSON.stringify(error.responseBody));
|
|
2164
|
-
throw error;
|
|
2165
|
-
}
|
|
2166
|
-
finally {
|
|
2167
|
-
span.end();
|
|
2168
|
-
}
|
|
2169
|
-
});
|
|
2170
|
-
}
|
|
2171
|
-
// ─── Adaptive fallback matrix (per write task) ────────────────────────────
|
|
2172
|
-
//
|
|
2173
|
-
// Every entity write goes through three layers, applied in this order:
|
|
2174
|
-
//
|
|
2175
|
-
// (a) `withTransientRetry` — retries once on 500/502/503/504 with a 1s
|
|
2176
|
-
// back-off. Per-call, isolated from other
|
|
2177
|
-
// tasks. 4xx is never retried (deterministic).
|
|
2178
|
-
// (b) `POST 409 → PATCH` — the create call hit a row with the same
|
|
2179
|
-
// code; the platform already has it. Convert
|
|
2180
|
-
// to a PATCH and continue. Pre-existing.
|
|
2181
|
-
// (c) `PATCH 404 → POST` — the patch call hit "not found"; baseline
|
|
2182
|
-
// drifted (item never landed on the platform
|
|
2183
|
-
// from a prior partial push). Convert to a
|
|
2184
|
-
// POST so the row reappears. NEW.
|
|
2185
|
-
// (d) `DELETE 404 → success` — the delete target is already gone. The
|
|
2186
|
-
// desired end state is reached. Treat as
|
|
2187
|
-
// success and log "(already absent)" so the
|
|
2188
|
-
// user can see what happened. NEW.
|
|
2189
|
-
//
|
|
2190
|
-
// All four layers are visible in the per-task push log files under
|
|
2191
|
-
// `.aui/push-logs/` so the BFF / agent-builder-bff can audit decisions.
|
|
2192
|
-
function isAlreadyExistsConflict(err) {
|
|
2193
|
-
if (!err || typeof err !== "object")
|
|
2194
|
-
return false;
|
|
2195
|
-
const code = err.statusCode
|
|
2196
|
-
?? err.status;
|
|
2197
|
-
return code === 409;
|
|
2198
|
-
}
|
|
2199
|
-
function isNotFoundError(err) {
|
|
2200
|
-
if (!err || typeof err !== "object")
|
|
2201
|
-
return false;
|
|
2202
|
-
const code = err.statusCode
|
|
2203
|
-
?? err.status;
|
|
2204
|
-
return code === 404;
|
|
2205
|
-
}
|
|
2206
|
-
function isTransient5xx(err) {
|
|
2207
|
-
if (!err || typeof err !== "object")
|
|
2208
|
-
return false;
|
|
2209
|
-
const code = err.statusCode
|
|
2210
|
-
?? err.status;
|
|
2211
|
-
return code === 500 || code === 502 || code === 503 || code === 504;
|
|
2212
|
-
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Tag the currently-active span with a fallback-decision event + attribute,
|
|
2215
|
-
* so Logfire shows exactly which adaptive layer fired during a push.
|
|
2216
|
-
*
|
|
2217
|
-
* Useful queries once published:
|
|
2218
|
-
* - `attributes."push.task.fallback":"patch_404_to_post"` → every drift
|
|
2219
|
-
* recovery (next push self-healed a previously-failed POST).
|
|
2220
|
-
* - `attributes."push.task.fallback":"transient_retry"` → backend 5xx
|
|
2221
|
-
* events that were absorbed by the retry layer.
|
|
2222
|
-
* - `attributes."push.task.fallback":"post_409_to_patch"` → "create"
|
|
2223
|
-
* calls that converted to "update" because the row pre-existed.
|
|
2224
|
-
* - `attributes."push.task.fallback":"delete_404_already_absent"` →
|
|
2225
|
-
* deletes that no-op'd because the row was already gone.
|
|
2226
|
-
*
|
|
2227
|
-
* No-op when there's no active span (e.g. unit tests outside the push flow).
|
|
2228
|
-
*/
|
|
2229
|
-
function recordFallbackEvent(kind, detail) {
|
|
2230
|
-
const span = trace.getActiveSpan();
|
|
2231
|
-
if (!span)
|
|
2232
|
-
return;
|
|
2233
|
-
span.addEvent(`fallback.${kind}`, detail);
|
|
2234
|
-
span.setAttribute("push.task.fallback", kind);
|
|
2235
|
-
for (const [k, v] of Object.entries(detail ?? {})) {
|
|
2236
|
-
span.setAttribute(`push.task.fallback.${k}`, v);
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
/**
|
|
2240
|
-
* Run one entity-settings write call once, and retry exactly once on a
|
|
2241
|
-
* transient 5xx after a 1s back-off. The snapshot upload has its own
|
|
2242
|
-
* retry loop (see `pushSnapshot`); this is the equivalent for individual
|
|
2243
|
-
* agent-settings writes. Never retries on 4xx — those are deterministic.
|
|
2244
|
-
*/
|
|
2245
|
-
async function withTransientRetry(label, fn) {
|
|
2246
|
-
try {
|
|
2247
|
-
return await fn();
|
|
2248
|
-
}
|
|
2249
|
-
catch (err) {
|
|
2250
|
-
if (!isTransient5xx(err))
|
|
2251
|
-
throw err;
|
|
2252
|
-
const code = err.statusCode
|
|
2253
|
-
?? err.status;
|
|
2254
|
-
if (process.env.AUI_DEBUG) {
|
|
2255
|
-
console.log(`[debug] ${label} got ${code}, retrying once after 1000ms`);
|
|
2256
|
-
}
|
|
2257
|
-
recordFallbackEvent("transient_retry", {
|
|
2258
|
-
label,
|
|
2259
|
-
status_code: code ?? 0,
|
|
2260
|
-
backoff_ms: 1000,
|
|
2261
|
-
});
|
|
2262
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
2263
|
-
return await fn();
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
/**
|
|
2267
|
-
* A delete that has been short-circuited because the row was already absent
|
|
2268
|
-
* on the platform. Returned as a successful resolution so callers don't
|
|
2269
|
-
* count the task as failed, but tagged so the per-task log line can show
|
|
2270
|
-
* "(already absent)" instead of a generic ✓.
|
|
2271
|
-
*/
|
|
2272
|
-
const DELETE_ALREADY_ABSENT = Object.freeze({
|
|
2273
|
-
__aui_already_absent__: true,
|
|
2274
|
-
message: "Already absent on platform — treated as success",
|
|
2275
|
-
});
|
|
2276
|
-
function isAlreadyAbsentResult(value) {
|
|
2277
|
-
return (!!value
|
|
2278
|
-
&& typeof value === "object"
|
|
2279
|
-
&& value.__aui_already_absent__ === true);
|
|
2280
|
-
}
|
|
2281
|
-
async function _executePushTask(client, params, task) {
|
|
2282
|
-
switch (task.type) {
|
|
2283
|
-
case "patch-tool":
|
|
2284
|
-
return withTransientRetry(`PATCH tool ${task.toolName}`, async () => {
|
|
2285
|
-
try {
|
|
2286
|
-
return await client.patchTool(params, task.toolName, task.body);
|
|
2287
|
-
}
|
|
2288
|
-
catch (err) {
|
|
2289
|
-
if (isNotFoundError(err)) {
|
|
2290
|
-
if (process.env.AUI_DEBUG) {
|
|
2291
|
-
console.log(`[debug] patch-tool ${task.toolName}: 404 not found, falling back to POST`);
|
|
2292
|
-
}
|
|
2293
|
-
recordFallbackEvent("patch_404_to_post", { task_type: "patch-tool", tool: String(task.toolName ?? "") });
|
|
2294
|
-
return client.createTool(params, task.body);
|
|
2295
|
-
}
|
|
2296
|
-
throw err;
|
|
2297
|
-
}
|
|
2298
|
-
});
|
|
2299
|
-
case "create-tool":
|
|
2300
|
-
return withTransientRetry(`POST tool ${task.toolName ?? task.itemCode}`, async () => {
|
|
2301
|
-
try {
|
|
2302
|
-
return await client.createTool(params, task.body);
|
|
2303
|
-
}
|
|
2304
|
-
catch (err) {
|
|
2305
|
-
if (isAlreadyExistsConflict(err)) {
|
|
2306
|
-
if (process.env.AUI_DEBUG) {
|
|
2307
|
-
console.log(`[debug] create-tool: 409 already-exists, falling back to PATCH`);
|
|
2308
|
-
}
|
|
2309
|
-
const body = task.body;
|
|
2310
|
-
const toolCode = body.code || "";
|
|
2311
|
-
const toolName = toolCode.toUpperCase().replace(/-/g, "_");
|
|
2312
|
-
recordFallbackEvent("post_409_to_patch", { task_type: "create-tool", tool: toolName });
|
|
2313
|
-
return client.patchTool(params, toolName, body);
|
|
2314
|
-
}
|
|
2315
|
-
throw err;
|
|
2316
|
-
}
|
|
2317
|
-
});
|
|
2318
|
-
case "delete-tool":
|
|
2319
|
-
return withTransientRetry(`DELETE tool ${task.toolName}`, async () => {
|
|
2320
|
-
try {
|
|
2321
|
-
return await client.deleteTool(params, task.toolName);
|
|
2322
|
-
}
|
|
2323
|
-
catch (err) {
|
|
2324
|
-
if (isNotFoundError(err)) {
|
|
2325
|
-
if (process.env.AUI_DEBUG) {
|
|
2326
|
-
console.log(`[debug] delete-tool ${task.toolName}: 404 already absent`);
|
|
2327
|
-
}
|
|
2328
|
-
recordFallbackEvent("delete_404_already_absent", { task_type: "delete-tool", tool: String(task.toolName ?? "") });
|
|
2329
|
-
return DELETE_ALREADY_ABSENT;
|
|
2330
|
-
}
|
|
2331
|
-
throw err;
|
|
2332
|
-
}
|
|
2333
|
-
});
|
|
2334
|
-
case "patch-general-settings":
|
|
2335
|
-
return withTransientRetry("PATCH general-settings", () => client.patchGeneralSettings(params, task.body));
|
|
2336
|
-
case "put-parameters":
|
|
2337
|
-
return withTransientRetry("PUT parameters", () => client.putParameters(params, task.body, task.oldBody));
|
|
2338
|
-
case "put-entities":
|
|
2339
|
-
return withTransientRetry("PUT entities", () => client.putEntities(params, task.body, task.oldBody));
|
|
2340
|
-
case "put-integrations":
|
|
2341
|
-
return withTransientRetry("PUT integrations", () => client.putIntegrations(params, task.body, task.oldBody));
|
|
2342
|
-
case "create-parameter":
|
|
2343
|
-
return withTransientRetry(`POST param ${task.itemCode}`, async () => {
|
|
2344
|
-
try {
|
|
2345
|
-
return await client.createParameter(params, task.body);
|
|
2346
|
-
}
|
|
2347
|
-
catch (err) {
|
|
2348
|
-
if (isAlreadyExistsConflict(err)) {
|
|
2349
|
-
if (process.env.AUI_DEBUG) {
|
|
2350
|
-
console.log(`[debug] create-parameter ${task.itemCode}: 409, falling back to PATCH`);
|
|
2351
|
-
}
|
|
2352
|
-
recordFallbackEvent("post_409_to_patch", { task_type: "create-parameter", code: String(task.itemCode ?? "") });
|
|
2353
|
-
return client.patchParameter(params, task.itemCode, task.body);
|
|
2354
|
-
}
|
|
2355
|
-
throw err;
|
|
2356
|
-
}
|
|
2357
|
-
});
|
|
2358
|
-
case "patch-parameter":
|
|
2359
|
-
return withTransientRetry(`PATCH param ${task.itemCode}`, async () => {
|
|
2360
|
-
try {
|
|
2361
|
-
return await client.patchParameter(params, task.itemCode, task.body);
|
|
2362
|
-
}
|
|
2363
|
-
catch (err) {
|
|
2364
|
-
if (isNotFoundError(err)) {
|
|
2365
|
-
if (process.env.AUI_DEBUG) {
|
|
2366
|
-
console.log(`[debug] patch-parameter ${task.itemCode}: 404 not found, falling back to POST`);
|
|
2367
|
-
}
|
|
2368
|
-
recordFallbackEvent("patch_404_to_post", { task_type: "patch-parameter", code: String(task.itemCode ?? "") });
|
|
2369
|
-
return client.createParameter(params, task.body);
|
|
2370
|
-
}
|
|
2371
|
-
throw err;
|
|
2372
|
-
}
|
|
2373
|
-
});
|
|
2374
|
-
case "delete-parameter":
|
|
2375
|
-
return withTransientRetry(`DELETE param ${task.itemCode}`, async () => {
|
|
2376
|
-
try {
|
|
2377
|
-
return await client.deleteParameter(params, task.itemCode, task.body);
|
|
2378
|
-
}
|
|
2379
|
-
catch (err) {
|
|
2380
|
-
if (isNotFoundError(err)) {
|
|
2381
|
-
if (process.env.AUI_DEBUG) {
|
|
2382
|
-
console.log(`[debug] delete-parameter ${task.itemCode}: 404 already absent`);
|
|
2383
|
-
}
|
|
2384
|
-
recordFallbackEvent("delete_404_already_absent", { task_type: "delete-parameter", code: String(task.itemCode ?? "") });
|
|
2385
|
-
return DELETE_ALREADY_ABSENT;
|
|
2386
|
-
}
|
|
2387
|
-
throw err;
|
|
2388
|
-
}
|
|
2389
|
-
});
|
|
2390
|
-
case "create-entity":
|
|
2391
|
-
return withTransientRetry(`POST entity ${task.itemCode}`, async () => {
|
|
2392
|
-
try {
|
|
2393
|
-
return await client.createEntity(params, task.body);
|
|
2394
|
-
}
|
|
2395
|
-
catch (err) {
|
|
2396
|
-
if (isAlreadyExistsConflict(err)) {
|
|
2397
|
-
if (process.env.AUI_DEBUG) {
|
|
2398
|
-
console.log(`[debug] create-entity ${task.itemCode}: 409, falling back to PATCH`);
|
|
2399
|
-
}
|
|
2400
|
-
recordFallbackEvent("post_409_to_patch", { task_type: "create-entity", code: String(task.itemCode ?? "") });
|
|
2401
|
-
return client.patchEntity(params, task.itemCode, task.body);
|
|
2402
|
-
}
|
|
2403
|
-
throw err;
|
|
2404
|
-
}
|
|
2405
|
-
});
|
|
2406
|
-
case "patch-entity":
|
|
2407
|
-
return withTransientRetry(`PATCH entity ${task.itemCode}`, async () => {
|
|
2408
|
-
try {
|
|
2409
|
-
return await client.patchEntity(params, task.itemCode, task.body);
|
|
2410
|
-
}
|
|
2411
|
-
catch (err) {
|
|
2412
|
-
if (isNotFoundError(err)) {
|
|
2413
|
-
if (process.env.AUI_DEBUG) {
|
|
2414
|
-
console.log(`[debug] patch-entity ${task.itemCode}: 404, falling back to POST`);
|
|
2415
|
-
}
|
|
2416
|
-
recordFallbackEvent("patch_404_to_post", { task_type: "patch-entity", code: String(task.itemCode ?? "") });
|
|
2417
|
-
return client.createEntity(params, task.body);
|
|
2418
|
-
}
|
|
2419
|
-
throw err;
|
|
2420
|
-
}
|
|
2421
|
-
});
|
|
2422
|
-
case "delete-entity":
|
|
2423
|
-
return withTransientRetry(`DELETE entity ${task.itemCode}`, async () => {
|
|
2424
|
-
try {
|
|
2425
|
-
return await client.deleteEntity(params, task.itemCode);
|
|
2426
|
-
}
|
|
2427
|
-
catch (err) {
|
|
2428
|
-
if (isNotFoundError(err)) {
|
|
2429
|
-
if (process.env.AUI_DEBUG) {
|
|
2430
|
-
console.log(`[debug] delete-entity ${task.itemCode}: 404 already absent`);
|
|
2431
|
-
}
|
|
2432
|
-
recordFallbackEvent("delete_404_already_absent", { task_type: "delete-entity", code: String(task.itemCode ?? "") });
|
|
2433
|
-
return DELETE_ALREADY_ABSENT;
|
|
2434
|
-
}
|
|
2435
|
-
throw err;
|
|
2436
|
-
}
|
|
2437
|
-
});
|
|
2438
|
-
case "create-integration":
|
|
2439
|
-
return withTransientRetry(`POST integration ${task.itemCode}`, async () => {
|
|
2440
|
-
try {
|
|
2441
|
-
return await client.createIntegration(params, task.body);
|
|
2442
|
-
}
|
|
2443
|
-
catch (err) {
|
|
2444
|
-
if (isAlreadyExistsConflict(err)) {
|
|
2445
|
-
if (process.env.AUI_DEBUG) {
|
|
2446
|
-
console.log(`[debug] create-integration ${task.itemCode}: 409, falling back to PATCH`);
|
|
2447
|
-
}
|
|
2448
|
-
recordFallbackEvent("post_409_to_patch", { task_type: "create-integration", code: String(task.itemCode ?? "") });
|
|
2449
|
-
return client.patchIntegration(params, task.itemCode, task.body);
|
|
2450
|
-
}
|
|
2451
|
-
throw err;
|
|
2452
|
-
}
|
|
2453
|
-
});
|
|
2454
|
-
case "patch-integration":
|
|
2455
|
-
return withTransientRetry(`PATCH integration ${task.itemCode}`, async () => {
|
|
2456
|
-
try {
|
|
2457
|
-
return await client.patchIntegration(params, task.itemCode, task.body);
|
|
2458
|
-
}
|
|
2459
|
-
catch (err) {
|
|
2460
|
-
if (isNotFoundError(err)) {
|
|
2461
|
-
if (process.env.AUI_DEBUG) {
|
|
2462
|
-
console.log(`[debug] patch-integration ${task.itemCode}: 404 not found, falling back to POST`);
|
|
2463
|
-
}
|
|
2464
|
-
recordFallbackEvent("patch_404_to_post", { task_type: "patch-integration", code: String(task.itemCode ?? "") });
|
|
2465
|
-
return client.createIntegration(params, task.body);
|
|
2466
|
-
}
|
|
2467
|
-
throw err;
|
|
2468
|
-
}
|
|
2469
|
-
});
|
|
2470
|
-
case "delete-integration":
|
|
2471
|
-
return withTransientRetry(`DELETE integration ${task.itemCode}`, async () => {
|
|
2472
|
-
try {
|
|
2473
|
-
return await client.deleteIntegration(params, task.itemCode);
|
|
2474
|
-
}
|
|
2475
|
-
catch (err) {
|
|
2476
|
-
if (isNotFoundError(err)) {
|
|
2477
|
-
if (process.env.AUI_DEBUG) {
|
|
2478
|
-
console.log(`[debug] delete-integration ${task.itemCode}: 404 already absent`);
|
|
2479
|
-
}
|
|
2480
|
-
recordFallbackEvent("delete_404_already_absent", { task_type: "delete-integration", code: String(task.itemCode ?? "") });
|
|
2481
|
-
return DELETE_ALREADY_ABSENT;
|
|
2482
|
-
}
|
|
2483
|
-
throw err;
|
|
2484
|
-
}
|
|
2485
|
-
});
|
|
2486
|
-
case "put-rules":
|
|
2487
|
-
return withTransientRetry("PUT rules", () => client.putRules(params, task.body));
|
|
2488
|
-
default:
|
|
2489
|
-
throw new Error(`Unknown push task type: ${task.type}`);
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
1725
|
//# sourceMappingURL=push.js.map
|