nendlabsExperiments

events, aggregation, reports

logst

Model-written operational summaries only become useful after events are constrained, scoped, aggregated, and turned into bounded context.

From Logs to Bounded Context

The weak version of event intelligence is a model staring at logs. That is not the interesting system. Raw events need shape before generated writing can be trusted.

Logst is three pieces: a Remix API for organization-scoped events, a sidecar runtime for background work, and a client SDK that keeps event capture disciplined at the edge. The model is downstream of that system. It should see bounded aggregates, not an arbitrary stream of payloads.

The current code implements a narrower and more useful claim than the homepage language around embeddings or long-range reasoning. The inspected report generator works over six-hour windows, thirty-minute buckets, deltas, trends, spike signals, structured insights, and bounded final assets.

The Ingestion Contract

The event endpoint accepts either a single event shape or a batch, but never both at once. It resolves organization and access token from the request context, rejects invalid JSON, rejects events without names, and rejects nested objects or arrays in properties.

Scalar-only properties are a real design decision. They make event capture portable, keep SDK behavior simple, and prevent report generation from depending on arbitrary payload trees. Batch creation is transactional, so downstream reports do not inherit partial-write ambiguity.

typescript
if (payload.events && (payload.name || payload.properties)) {
  return json(
    {
      success: false,
      message: "Cannot provide both root-level event fields and an array of events together.",
    },
    { status: 400 },
  );
}

function validatePropertiesScalar(properties?: Record<string, unknown>) {
  if (!properties) return true;

  for (const val of Object.values(properties)) {
    if (val !== null && !["string", "number", "boolean"].includes(typeof val)) {
      return false;
    }
  }

  return true;
}

const createdEvents = await createEvents({
  organizationId,
  events: eventsPayload,
  accessTokenId,
});

The Client Protects the Edge

The SDK is not just convenience. It is where sloppy event capture is prevented before it enters the system. Event names are sanitized, common properties are merged centrally, events are queued, and publishing can happen immediately or through a timer.

Teardown is delivery behavior. Destroying the client clears the timer and makes a final publish attempt. That matters in short-lived processes where losing the last buffered events would make reports look calmer than reality.

typescript
const sanitizedName = name
  .replace(/[^a-zA-Z0-9_:\-/]/g, ".")
  .replace(/(\.)\1+/g, ".")
  .replace(/^\.+|\.+$/g, "");

this.queue.push({
  name: sanitizedName,
  properties: {
    ...this.commonEventProperties,
    ...(properties ?? {}),
  },
});

if (this.publishImmediate) {
  this.publishEvents();
} else {
  this.schedulePublish();
}

The Sidecar Is the Operator

The sidecar is a separate operating surface. It has its own API clients, logger namespace, task runner, internal RPC client, and the ability to log its own work back into Logst. That split is the right shape for background maintenance and reporting experiments because it keeps scheduled work out of the request path.

The honest current state is visible in the task registry. Pruning and RPC health checks are active. Load testing and report generation are implemented but disabled. That makes the page stronger, not weaker, because it separates built machinery from running service behavior.

typescript
export const tasks = {
  "prune-events": pruneEvents,
  "rpc-health-check": rpcHealthCheck,
  // "logst-load-test-events": logstLoadTestEvents,
  // "schedule-report-generation": scheduleReportGeneration,
};

Reports Are Computed Before They Are Written

The report generator earns the model call by doing the mechanical work first. It reads a six-hour window, splits it into two halves, computes count changes per event type, builds thirty-minute sub-intervals, and marks spikes only when both relative and absolute thresholds are crossed.

Only after that aggregation does generation enter. The first pass asks for insights over the bounded metrics. The second pass produces multiple assets: a short markdown report, a structured system report, and a Discord-sized message. The model is a writer over prepared context, not the database query engine.

typescript
const changePercentage =
  firstHalfCount > 0
    ? ((secondHalfCount - firstHalfCount) / firstHalfCount) * 100
    : secondHalfCount > 0
      ? 100
      : 0;

const intervalDuration = 30 * 60 * 1000;
const averageSubInterval =
  subIntervals.reduce((a, b) => a + b, 0) / (subIntervals.length || 1);
const maxSubInterval = Math.max(...subIntervals);

const spikeAlert =
  maxSubInterval > averageSubInterval * 2 && maxSubInterval > 50;

The Boundary Is Honest

The commit history tells a useful arc: Remix starter, organization-scoped event API, monorepo move, sidecar, internal RPC, first report task, model-assisted report summaries, structured report UI, SDK client, then daily reports paused. That is a better story than pretending the current system is a polished reporting product.

The checked-in database artifact is sparse, tests are thin, and the authenticated report page is sample data rather than live generated reports. The experiment should be read as the architecture of a useful event-to-report loop: constrained ingestion, disciplined client behavior, sidecar operation, aggregate context, and bounded model-written outputs.