@nurix/nustack 0.23.8 → 0.23.10

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.
@@ -14,8 +14,9 @@ import { INPUTS_FILE, LOCAL_OUTPUT_HEADER, renderCentralConfig, renderInputsFile
14
14
  import { applyConfigChange, removeInputFile } from "./telemetry/reload.js";
15
15
  import { discoverProviderBindings } from "./telemetry/provider_paths.js";
16
16
  import { reportTelemetryHealth } from "./telemetry/health_report.js";
17
+ import { DEFAULT_SESSION_INDEX, isSameOutput, machineVerdict, partAnswerOf, readPartState, recordPartState, } from "./telemetry/part_state.js";
17
18
  const TELEMETRY_SERVICE = "elastic-observability";
18
- const DEFAULT_INDEX = "claude-sessions";
19
+ const DEFAULT_INDEX = DEFAULT_SESSION_INDEX;
19
20
  const NO_OP_LOG = () => { };
20
21
  function defaultSeams() {
21
22
  return {
@@ -123,53 +124,68 @@ export async function ensureBindingTelemetry(opts) {
123
124
  await reportBestEffort(baseUrl, store, seams, accessToken);
124
125
  return;
125
126
  }
126
- let brokerFailure = null;
127
+ let brokerError = null;
127
128
  const credential = accessToken
128
129
  ? await fetchBrokeredCredential(baseUrl, accessToken, {
129
130
  projectId: binding.projectId,
130
131
  service: TELEMETRY_SERVICE,
131
132
  }).catch((error) => {
132
- brokerFailure = error instanceof Error ? error.message : String(error);
133
+ brokerError = error;
133
134
  return null;
134
135
  })
135
136
  : null;
136
- if (!credential?.url && !localOutput) {
137
+ const brokerFailure = brokerError === null ? null : message(brokerError);
138
+ const answer = accessToken ? partAnswerOf(binding.organizationId, { credential, error: brokerError }) : null;
139
+ const parts = answer ? recordPartState({ projects: { [binding.projectId]: answer } }) : readPartState();
140
+ const candidate = credential?.url && answer ? { destination: answer.destination ?? "nurix", url: credential.url, index: answer.index ?? DEFAULT_INDEX } : null;
141
+ const verdict = machineVerdict(bindingsOf(store, binding.id, true), parts, candidate);
142
+ const output = verdict.selection.output;
143
+ if (verdict.selection.conflict) {
144
+ recordPartState({ conflict: true });
145
+ await applyInputs(store, binding.id, verdict, deviceRef, userId, seams);
146
+ disableActivity(store, binding.id);
147
+ recordDegraded(store, "destination_conflict");
148
+ await reportBestEffort(baseUrl, store, seams, accessToken);
149
+ return;
150
+ }
151
+ if (!output) {
152
+ recordPartState({ conflict: false });
153
+ if (answer?.state === "off") {
154
+ await applyInputs(store, binding.id, verdict, deviceRef, userId, seams);
155
+ disableActivity(store, binding.id);
156
+ }
137
157
  const unserved = brokerFailure !== null && brokerFailure.includes("does not serve");
138
158
  recordDegraded(store, unserved ? "credential_plane_unserved" : "no_output_credential");
139
159
  await reportBestEffort(baseUrl, store, seams, accessToken);
140
160
  return;
141
161
  }
142
- if (credential?.url) {
143
- const index = credential.meta?.index || DEFAULT_INDEX;
162
+ if (credential && candidate && isSameOutput(output, candidate)) {
144
163
  const central = {
145
164
  relativePath: "filebeat.yml",
146
- contents: renderCentralConfig({ elkUrl: credential.url, apiKey: credential.key, index }),
165
+ contents: renderCentralConfig({ elkUrl: candidate.url, apiKey: credential.key, index: candidate.index }),
147
166
  };
148
- const centralApply = await applyConfigChange({
149
- files: [central],
150
- scope: "central",
151
- context: centralReloadContext(seams, filebeatDir()),
152
- });
153
- if (!centralApply.ok) {
154
- recordDegraded(store, centralApply.errorCode);
155
- await reportBestEffort(baseUrl, store, seams, accessToken);
156
- return;
167
+ const live = await readFile(mainConfigPath(), "utf8").catch(() => null);
168
+ if (live !== central.contents) {
169
+ const centralApply = await applyConfigChange({
170
+ files: [central],
171
+ scope: "central",
172
+ context: centralReloadContext(seams, filebeatDir()),
173
+ });
174
+ if (!centralApply.ok) {
175
+ recordDegraded(store, centralApply.errorCode);
176
+ await reportBestEffort(baseUrl, store, seams, accessToken);
177
+ return;
178
+ }
157
179
  }
158
180
  }
181
+ recordPartState({ output, conflict: false });
159
182
  if (!seams.serviceRegistered())
160
183
  await seams.registerService();
161
- const inputs = renderInputs(store, { id: binding.id, binding, enabled: true }, deviceRef, userId) ?? "";
162
- await removeOrphanInputs();
163
- const inputsApply = await applyConfigChange({
164
- files: [{ relativePath: INPUTS_FILE, contents: inputs }],
165
- scope: "inputs",
166
- context: seams.reloadContext(filebeatDir()),
167
- });
168
- if (!inputsApply.ok) {
169
- recordDegraded(store, inputsApply.errorCode);
184
+ if (!(await applyInputs(store, binding.id, verdict, deviceRef, userId, seams, binding))) {
170
185
  await reportBestEffort(baseUrl, store, seams, accessToken);
171
186
  return;
172
187
  }
188
+ const isShipping = verdict.shipping.has(binding.id);
173
189
  for (const discovered of discoverProviderBindings(binding.repositoryRoot)) {
174
190
  upsertActivityBinding(store, {
175
191
  id: activityRowId(binding.id, discovered.provider, discovered.pathFingerprint),
@@ -177,7 +193,7 @@ export async function ensureBindingTelemetry(opts) {
177
193
  provider: discovered.provider,
178
194
  providerProjectPath: discovered.absolutePath,
179
195
  pathFingerprint: discovered.pathFingerprint,
180
- state: discovered.state,
196
+ state: isShipping ? discovered.state : "disabled",
181
197
  });
182
198
  }
183
199
  upsertFilebeatState(store, {
@@ -226,6 +242,7 @@ export async function setLocalOutput(opts) {
226
242
  recordDegraded(store, centralApply.errorCode);
227
243
  return { ok: false, errorCode: centralApply.errorCode };
228
244
  }
245
+ recordPartState({ output: { destination: "local", url: opts.elkUrl, index: opts.index || DEFAULT_INDEX }, conflict: false });
229
246
  if (!seams.serviceRegistered())
230
247
  await seams.registerService();
231
248
  upsertFilebeatState(store, {
@@ -251,7 +268,8 @@ export async function removeBindingTelemetry(opts) {
251
268
  try {
252
269
  const device = readDeviceState(store);
253
270
  await removeOrphanInputs();
254
- const inputs = renderInputs(store, { id: bindingId, enabled: false }, device?.deviceRef ?? "", device?.activeUserId ?? "");
271
+ const verdict = machineVerdict(bindingsOf(store, bindingId, false), readPartState());
272
+ const inputs = renderInputs(store, { id: bindingId, enabled: false }, device?.deviceRef ?? "", device?.activeUserId ?? "", verdict);
255
273
  if (inputs === null) {
256
274
  await removeInputFile(filebeatDir(), INPUTS_FILE);
257
275
  }
@@ -266,20 +284,70 @@ export async function removeBindingTelemetry(opts) {
266
284
  recordDegraded(store, applied.errorCode);
267
285
  }
268
286
  }
269
- for (const row of listActivityBindings(store, bindingId)) {
270
- upsertActivityBinding(store, {
271
- id: row.id,
272
- bindingId: row.bindingId,
273
- provider: row.provider,
274
- providerProjectPath: row.providerProjectPath,
275
- pathFingerprint: row.pathFingerprint,
276
- state: "disabled",
277
- });
287
+ disableActivity(store, bindingId);
288
+ }
289
+ catch {
290
+ }
291
+ }
292
+ function disableActivity(store, bindingId) {
293
+ for (const row of listActivityBindings(store, bindingId)) {
294
+ upsertActivityBinding(store, {
295
+ id: row.id,
296
+ bindingId: row.bindingId,
297
+ provider: row.provider,
298
+ providerProjectPath: row.providerProjectPath,
299
+ pathFingerprint: row.pathFingerprint,
300
+ state: "disabled",
301
+ });
302
+ }
303
+ }
304
+ function bindingsOf(store, bindingId, folderEnabled) {
305
+ return listActiveBindings(store).map((b) => ({ bindingId: b.id, projectId: b.projectId, folderEnabled: b.id === bindingId ? folderEnabled : b.telemetryEnabled }));
306
+ }
307
+ async function applyInputs(store, bindingId, verdict, deviceRef, userId, seams, binding) {
308
+ const inputs = renderInputs(store, { id: bindingId, binding, enabled: true }, deviceRef, userId, verdict) ?? "";
309
+ await removeOrphanInputs();
310
+ const inputsApply = await applyConfigChange({
311
+ files: [{ relativePath: INPUTS_FILE, contents: inputs }],
312
+ scope: "inputs",
313
+ context: seams.reloadContext(filebeatDir()),
314
+ });
315
+ if (!inputsApply.ok)
316
+ recordDegraded(store, inputsApply.errorCode);
317
+ return inputsApply.ok;
318
+ }
319
+ export async function reconcileAllBindings(opts) {
320
+ const { store, baseUrl } = opts;
321
+ const bindings = listActiveBindings(store);
322
+ let reconciled = 0;
323
+ try {
324
+ const machine = readFilebeatState(store);
325
+ if (!optedOutByEnv() && (!machine || machine.telemetryEnabled)) {
326
+ for (const binding of bindings) {
327
+ await ensureBindingTelemetry({ baseUrl, store, binding, seams: opts.seams });
328
+ reconciled += 1;
329
+ }
278
330
  }
279
331
  }
332
+ finally {
333
+ recordPartStateBestEffort();
334
+ }
335
+ const parts = readPartState();
336
+ return { reconciled, parts: countParts(bindings, parts), conflict: parts.conflict };
337
+ }
338
+ function recordPartStateBestEffort() {
339
+ try {
340
+ recordPartState();
341
+ }
280
342
  catch {
281
343
  }
282
344
  }
345
+ function countParts(bindings, parts) {
346
+ const counts = { on: 0, off: 0, unknown: 0 };
347
+ for (const projectId of new Set(bindings.map((b) => b.projectId)))
348
+ counts[parts.projects[projectId]?.state ?? "unknown"] += 1;
349
+ return counts;
350
+ }
283
351
  export async function switchMachineOff(opts) {
284
352
  const seams = { ...defaultSeams(), ...opts.seams };
285
353
  const { store } = opts;
@@ -310,15 +378,15 @@ export async function switchMachineOff(opts) {
310
378
  upsertFilebeatState(store, { processState: "stopped" });
311
379
  return "stopped";
312
380
  }
313
- function renderInputs(store, reconciled, deviceRef, userId) {
381
+ function renderInputs(store, reconciled, deviceRef, userId, verdict) {
314
382
  const byId = new Map();
315
383
  for (const b of listActiveBindings(store)) {
316
- byId.set(b.id, { binding: toTelemetryBinding(b, deviceRef, userId), enabled: b.telemetryEnabled });
384
+ byId.set(b.id, { binding: toTelemetryBinding(b, deviceRef, userId), enabled: b.telemetryEnabled && verdict.shipping.has(b.id) });
317
385
  }
318
386
  const known = reconciled.binding ? toTelemetryBinding(reconciled.binding, deviceRef, userId) : byId.get(reconciled.id)?.binding;
319
387
  if (known)
320
- byId.set(reconciled.id, { binding: known, enabled: reconciled.enabled });
321
- return byId.size === 0 ? null : renderInputsFile([...byId.values()], { deviceRef, userId });
388
+ byId.set(reconciled.id, { binding: known, enabled: reconciled.enabled && verdict.shipping.has(reconciled.id) });
389
+ return byId.size === 0 ? null : renderInputsFile([...byId.values()], { deviceRef, userId }, { dropAll: verdict.dropCodex });
322
390
  }
323
391
  async function removeOrphanInputs() {
324
392
  const inputsDirPath = path.join(filebeatDir(), "inputs.d");
@@ -139,10 +139,10 @@ export function renderBindingInput(binding, enabled = true) {
139
139
  });
140
140
  return enabled ? block : block.replace("\n processors:\n", `\n processors:\n${SWITCHED_OFF}`);
141
141
  }
142
- export function renderCodexSharedInput(entries, device) {
142
+ export function renderCodexSharedInput(entries, device, options = {}) {
143
143
  const ordered = [...entries].sort((a, b) => a.binding.bindingId < b.binding.bindingId ? -1 : a.binding.bindingId > b.binding.bindingId ? 1 : 0);
144
144
  const sorted = ordered.filter((entry) => entry.enabled).map((entry) => entry.binding);
145
- const drops = sorted.length === 0 ? SWITCHED_OFF : "";
145
+ const drops = sorted.length === 0 || options.dropAll === true ? SWITCHED_OFF : "";
146
146
  const conditionals = sorted
147
147
  .map((binding) => `
148
148
  - if:
@@ -183,10 +183,10 @@ ${drops} - add_fields:
183
183
  ignore_missing: true
184
184
  `;
185
185
  }
186
- export function renderInputsFile(entries, device) {
186
+ export function renderInputsFile(entries, device, options = {}) {
187
187
  const sorted = [...entries].sort((a, b) => a.binding.bindingId < b.binding.bindingId ? -1 : a.binding.bindingId > b.binding.bindingId ? 1 : 0);
188
188
  const blocks = sorted.map((entry) => renderBindingInput(entry.binding, entry.enabled));
189
- return INPUTS_HEADER + blocks.join("") + renderCodexSharedInput(sorted, device);
189
+ return INPUTS_HEADER + blocks.join("") + renderCodexSharedInput(sorted, device, options);
190
190
  }
191
191
  export function splitInputBlocks(contents) {
192
192
  const starts = [...contents.matchAll(BLOCK_MARKER)].map((match) => ({
@@ -0,0 +1,2 @@
1
+ export const TELEMETRY_CHILD_VARIABLE = "NUSTACK_TELEMETRY_CHILD";
2
+ export const FILEBEAT_SERVICE_LABEL_VARIABLE = "NUSTACK_FILEBEAT_SERVICE_LABEL";
@@ -9,6 +9,7 @@ import { REDACTION_POLICY_VERSION, } from "./redaction_policy.js";
9
9
  import { INPUTS_FILE, TELEMETRY_CONFIG_VERSION, desiredConfigHash, renderBindingInput, splitInputBlocks, } from "./config_renderer.js";
10
10
  import { discoverProviderBindings } from "./provider_paths.js";
11
11
  import { readObservedConfigBytes } from "./reload.js";
12
+ import { machineVerdict, readPartState } from "./part_state.js";
12
13
  export function defaultProbes() {
13
14
  return {
14
15
  binaryPresent: () => existsSync(filebeatBinPath()),
@@ -36,7 +37,11 @@ export async function collectTelemetryStatus(store, probes = defaultProbes(), op
36
37
  const userId = device?.activeUserId ?? "";
37
38
  const folders = [];
38
39
  const activity = [];
39
- for (const binding of listActiveBindings(store)) {
40
+ const bindings = listActiveBindings(store);
41
+ const parts = readPartState();
42
+ const verdict = machineVerdict(bindings.map((b) => ({ bindingId: b.id, projectId: b.projectId, folderEnabled: b.telemetryEnabled })), parts);
43
+ for (const binding of bindings) {
44
+ const isRendered = verdict.shipping.has(binding.id);
40
45
  const rendered = renderBindingInput({
41
46
  bindingId: binding.id,
42
47
  repoRoot: binding.repositoryRoot,
@@ -57,9 +62,11 @@ export async function collectTelemetryStatus(store, probes = defaultProbes(), op
57
62
  desiredConfigHash: desired,
58
63
  observedConfigHash: observed,
59
64
  redactionPolicyVersion: REDACTION_POLICY_VERSION,
65
+ partState: parts.projects[binding.projectId]?.state ?? "unknown",
66
+ shipping: isRendered && verdict.selection.output !== null,
60
67
  state: folderState({
61
68
  telemetryEnabled,
62
- folderEnabled: binding.telemetryEnabled,
69
+ folderEnabled: binding.telemetryEnabled && isRendered,
63
70
  binaryPresent,
64
71
  desired,
65
72
  observed,
@@ -75,7 +82,7 @@ export async function collectTelemetryStatus(store, probes = defaultProbes(), op
75
82
  providerProjectKey: discovered.providerProjectKey,
76
83
  pathFingerprint: discovered.pathFingerprint,
77
84
  discoveryMethod: discovered.discoveryMethod,
78
- state: !telemetryEnabled || !binding.telemetryEnabled ? "disabled" : baseState,
85
+ state: !telemetryEnabled || !binding.telemetryEnabled || !isRendered ? "disabled" : baseState,
79
86
  lastObservedAtMs: row?.lastObservedAtMs ?? null,
80
87
  });
81
88
  }
@@ -0,0 +1,168 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { filebeatDir, mainConfigPath } from "../filebeat_installer.js";
6
+ import { locksDir } from "../local_store/paths.js";
7
+ import { DRAIN_CHILD_VARIABLE, isFlagSet } from "../records/environment.js";
8
+ import { BROKERED_OUTPUT_HEADER, LOCAL_OUTPUT_HEADER } from "./config_renderer.js";
9
+ import { TELEMETRY_CHILD_VARIABLE } from "./environment.js";
10
+ export const TELEMETRY_PART_OFF_REASON = "telemetry_part_off";
11
+ export const TELEMETRY_PART_DETAIL = "telemetryPart";
12
+ export const TELEMETRY_DESTINATION_DETAIL = "telemetryDestination";
13
+ export const DEFAULT_SESSION_INDEX = "claude-sessions";
14
+ export const PART_CHECK_INTERVAL_MS = 60 * 60 * 1000;
15
+ const PART_STATE_SCHEMA_VERSION = 1;
16
+ export function partStatePath() {
17
+ return path.join(filebeatDir(), "part-state.json");
18
+ }
19
+ export function telemetryReconcileLockPath() {
20
+ return path.join(locksDir(), "telemetry-reconcile.lock");
21
+ }
22
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23
+ const stringOrNull = (value) => (typeof value === "string" && value.length > 0 ? value : null);
24
+ function readOutput(value) {
25
+ if (!isRecord(value))
26
+ return null;
27
+ const destination = value.destination;
28
+ if (destination !== "nurix" && destination !== "tenant" && destination !== "local")
29
+ return null;
30
+ return { destination, url: stringOrNull(value.url), index: stringOrNull(value.index) };
31
+ }
32
+ function readProject(value) {
33
+ if (!isRecord(value) || (value.state !== "on" && value.state !== "off") || typeof value.organizationId !== "string")
34
+ return null;
35
+ const destination = value.destination === "nurix" || value.destination === "tenant" ? value.destination : null;
36
+ return { organizationId: value.organizationId, state: value.state, destination, url: stringOrNull(value.url), index: stringOrNull(value.index), checkedAt: String(value.checkedAt ?? "") };
37
+ }
38
+ function seedOutput() {
39
+ try {
40
+ const header = readFileSync(mainConfigPath(), "utf8").split("\n", 1)[0];
41
+ if (header === LOCAL_OUTPUT_HEADER)
42
+ return { destination: "local", url: null, index: null };
43
+ if (header === BROKERED_OUTPUT_HEADER)
44
+ return { destination: "nurix", url: null, index: null };
45
+ return null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ export function readPartState() {
52
+ const empty = { schemaVersion: PART_STATE_SCHEMA_VERSION, checkedAt: null, output: null, conflict: false, projects: {} };
53
+ let state = empty;
54
+ try {
55
+ const parsed = JSON.parse(readFileSync(partStatePath(), "utf8"));
56
+ if (isRecord(parsed) && parsed.schemaVersion === PART_STATE_SCHEMA_VERSION) {
57
+ const projects = {};
58
+ for (const [projectId, entry] of Object.entries(isRecord(parsed.projects) ? parsed.projects : {})) {
59
+ const project = readProject(entry);
60
+ if (project)
61
+ projects[projectId] = project;
62
+ }
63
+ state = { ...empty, checkedAt: stringOrNull(parsed.checkedAt), output: readOutput(parsed.output), conflict: parsed.conflict === true, projects };
64
+ }
65
+ }
66
+ catch {
67
+ }
68
+ return state.output ? state : { ...state, output: seedOutput() };
69
+ }
70
+ export function recordPartState(change = {}) {
71
+ const current = readPartState();
72
+ const projects = { ...current.projects };
73
+ for (const [projectId, entry] of Object.entries(change.projects ?? {})) {
74
+ if (entry)
75
+ projects[projectId] = entry;
76
+ else
77
+ delete projects[projectId];
78
+ }
79
+ const next = {
80
+ schemaVersion: PART_STATE_SCHEMA_VERSION,
81
+ checkedAt: new Date().toISOString(),
82
+ output: change.output === undefined ? current.output : change.output,
83
+ conflict: change.conflict ?? current.conflict,
84
+ projects,
85
+ };
86
+ const target = partStatePath();
87
+ mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
88
+ const staged = `${target}.${process.pid}.tmp`;
89
+ writeFileSync(staged, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
90
+ renameSync(staged, target);
91
+ return next;
92
+ }
93
+ export function partAnswerOf(organizationId, answer) {
94
+ const checkedAt = new Date().toISOString();
95
+ if (answer.credential) {
96
+ const index = typeof answer.credential.meta?.index === "string" && answer.credential.meta.index ? answer.credential.meta.index : DEFAULT_SESSION_INDEX;
97
+ const destination = answer.credential.meta?.destination === "tenant" ? "tenant" : "nurix";
98
+ return { organizationId, state: "on", destination, url: answer.credential.url, index, checkedAt };
99
+ }
100
+ const details = isRecord(answer.error) && isRecord(answer.error.details) ? answer.error.details : null;
101
+ if (!details)
102
+ return null;
103
+ if (details.reason === TELEMETRY_PART_OFF_REASON)
104
+ return { organizationId, state: "off", destination: null, url: null, index: null, checkedAt };
105
+ if (details[TELEMETRY_PART_DETAIL] !== "on")
106
+ return null;
107
+ const named = details[TELEMETRY_DESTINATION_DETAIL];
108
+ return { organizationId, state: "on", destination: named === "nurix" || named === "tenant" ? named : null, url: null, index: null, checkedAt };
109
+ }
110
+ export const isSameOutput = (a, b) => a !== null && b !== null && a.destination === b.destination && a.url === b.url && a.index === b.index;
111
+ export function matchesOutput(entry, output) {
112
+ if (!output || entry?.state === "off")
113
+ return false;
114
+ if (output.destination !== "tenant")
115
+ return entry?.destination !== "tenant";
116
+ return entry?.state === "on" && entry.destination === "tenant" && entry.url === output.url && entry.index === output.index;
117
+ }
118
+ export function selectMachineOutput(input) {
119
+ const live = input.bindings.filter((binding) => binding.folderEnabled && input.projects[binding.projectId]?.state !== "off");
120
+ const tenantOutputs = new Map();
121
+ for (const binding of live) {
122
+ const entry = input.projects[binding.projectId];
123
+ if (entry?.destination === "tenant" && entry.url !== null)
124
+ tenantOutputs.set(`${entry.url}\n${entry.index}`, { destination: "tenant", url: entry.url, index: entry.index });
125
+ }
126
+ if (tenantOutputs.size > 1)
127
+ return { output: input.current, shippingBindingIds: [], conflict: true };
128
+ const tenant = [...tenantOutputs.values()][0] ?? null;
129
+ let output = input.current;
130
+ if (tenant && (isSameOutput(tenant, input.candidate) || isSameOutput(tenant, input.current)))
131
+ output = tenant;
132
+ else if (!tenant && input.candidate?.destination === "nurix")
133
+ output = input.candidate;
134
+ const shippingBindingIds = live.filter((binding) => matchesOutput(input.projects[binding.projectId], output)).map((binding) => binding.bindingId);
135
+ return { output, shippingBindingIds, conflict: false };
136
+ }
137
+ export function machineVerdict(bindings, parts, candidate = null) {
138
+ const selection = selectMachineOutput({ bindings, projects: parts.projects, current: parts.output, candidate });
139
+ const isOff = (projectId) => parts.projects[projectId]?.state === "off";
140
+ const live = bindings.filter((binding) => binding.folderEnabled && !isOff(binding.projectId)).map((binding) => binding.bindingId);
141
+ const shipping = new Set(selection.conflict ? [] : selection.output === null ? live : selection.shippingBindingIds);
142
+ const dropCodex = selection.conflict || selection.output?.destination === "tenant" || bindings.some((binding) => isOff(binding.projectId));
143
+ return { selection, shipping, dropCodex };
144
+ }
145
+ export function isPartCheckStale(env = process.env, now = Date.now()) {
146
+ try {
147
+ if (isFlagSet(DRAIN_CHILD_VARIABLE, env) || isFlagSet(TELEMETRY_CHILD_VARIABLE, env) || env.NUSTACK_TELEMETRY === "off")
148
+ return false;
149
+ if (!statSync(mainConfigPath(), { throwIfNoEntry: false }))
150
+ return false;
151
+ const file = statSync(partStatePath(), { throwIfNoEntry: false });
152
+ return !file || now - file.mtimeMs > PART_CHECK_INTERVAL_MS;
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
158
+ export function startTelemetryReconcile(options = {}) {
159
+ try {
160
+ const entry = fileURLToPath(new URL("../../index.js", import.meta.url));
161
+ const args = [entry, ...(options.nustackUrl ? ["--nustack-url", options.nustackUrl] : []), "telemetry", "reconcile", "--quiet"];
162
+ const child = spawn(process.execPath, args, { detached: true, stdio: "ignore", env: { ...process.env, [TELEMETRY_CHILD_VARIABLE]: "1" } });
163
+ child.on("error", () => { });
164
+ child.unref();
165
+ }
166
+ catch {
167
+ }
168
+ }