Skip to content

Commit 5263e8b

Browse files
authored
Merge pull request #19 from pankaj4u4m/telemetry
Refactor memory handling to support optional agent IDs
2 parents d803c75 + b10c9e2 commit 5263e8b

24 files changed

Lines changed: 989 additions & 205 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ Atelier helps teams ship the same context runtime everywhere: on the command lin
88

99
The runtime separates **Passive Tracking** (enabled by default) from **Active Context** (requires Development Mode).
1010

11+
![Atelier Overview dashboard](docs/assets/screenshots/overview.png)
12+
1113
## Passive Tracking (Production Ready)
1214

1315
- **Sessions & Ledger** — track every agent run and execution state
1416
- **Expense Tracking** — aggregate token usage and estimated costs across all hosts
1517
- **Traces** — record observable execution history (files, commands, errors)
1618
- **Tools & Agents** — central registry of available capabilities and personas
1719

20+
![Cost & efficiency analytics across hosts, models, and tools](docs/assets/screenshots/analytics.png)
21+
1822
## Active Context (Development Mode)
1923

2024
_Enable with `ATELIER_DEV_MODE=1`_
208 KB
Loading
309 KB
Loading
106 KB
Loading
839 KB
Loading
605 KB
Loading

docs/assets/screenshots/tools.png

67.4 KB
Loading

frontend/src/pages/Traces.test.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,83 @@ describe("Traces page", () => {
109109
expect(screen.getByText(/run-timeout/i)).toBeInTheDocument();
110110
expect(screen.getByText(/Commands:/i)).toBeInTheDocument();
111111
});
112+
113+
it("hides stale or unrelated highlighted snippets for the current search", async () => {
114+
const user = userEvent.setup();
115+
vi.spyOn(globalThis, "fetch").mockImplementation(
116+
(input: RequestInfo | URL) => {
117+
const url = String(input);
118+
119+
if (url.includes("/api/traces")) {
120+
const params = new URL(url, "http://localhost").searchParams;
121+
const query = params.get("query");
122+
return Promise.resolve(
123+
jsonResponse({
124+
items: [
125+
{
126+
id: query ? "trace-sidecar" : "trace-base",
127+
session_id: query ? "run-sidecar" : "run-base",
128+
agent: "codex",
129+
host: "codex",
130+
domain: "coding",
131+
task: query ? "Investigate sidecar session" : "Base session",
132+
status: "success",
133+
files_touched: [],
134+
tools_called: [],
135+
commands_run: [],
136+
errors_seen: [],
137+
repeated_failures: [],
138+
validation_results: [],
139+
created_at: "2026-05-12T00:00:00Z",
140+
snippets: query
141+
? [
142+
"Tools: [[shopify]] sync service skills",
143+
"Commands: inspect [[sidecar]] process logs",
144+
]
145+
: [],
146+
},
147+
],
148+
metrics: {
149+
stats: {
150+
total: 1,
151+
success: 1,
152+
failed: 0,
153+
partial: 0,
154+
},
155+
hosts: ["codex"],
156+
domains: ["coding"],
157+
},
158+
})
159+
);
160+
}
161+
162+
return Promise.resolve(new Response("not found", { status: 404 }));
163+
}
164+
);
165+
166+
render(
167+
<MemoryRouter initialEntries={["/sessions"]}>
168+
<Routes>
169+
<Route path="/sessions" element={<Traces />} />
170+
</Routes>
171+
</MemoryRouter>
172+
);
173+
174+
await waitFor(() => {
175+
expect(screen.getByText("Base session")).toBeInTheDocument();
176+
});
177+
178+
await user.type(
179+
screen.getByPlaceholderText(
180+
/Search tasks, reasoning, tools, commands, files, validations, and summaries/i
181+
),
182+
"sidecar"
183+
);
184+
185+
expect(
186+
await screen.findByText("Investigate sidecar session")
187+
).toBeInTheDocument();
188+
expect(screen.getByText(/inspect/i)).toBeInTheDocument();
189+
expect(screen.queryByText(/shopify/i)).not.toBeInTheDocument();
190+
});
112191
});

frontend/src/pages/Traces.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState, useMemo } from "react";
1+
import { useEffect, useState, useMemo, useRef } from "react";
22
import { useSearchParams } from "react-router-dom";
33
import {
44
api,
@@ -27,6 +27,7 @@ export default function Traces() {
2727
const [query, setQuery] = useState<string>(initialQuery);
2828
const [searchInput, setSearchInput] = useState<string>(initialQuery);
2929
const [page, setPage] = useState(0);
30+
const tracesRequestSeq = useRef(0);
3031

3132
useEffect(() => {
3233
const urlQuery = searchParams.get("q") ?? "";
@@ -70,21 +71,28 @@ export default function Traces() {
7071

7172
// Fetch traces when filters change
7273
useEffect(() => {
74+
const requestSeq = ++tracesRequestSeq.current;
75+
let cancelled = false;
7376
setLoading(true);
7477
setPage(0);
7578
setErr(null);
7679
api
7780
.traces(50, 0, domainFilter, hostFilter, query)
7881
.then((res) => {
82+
if (cancelled || requestSeq !== tracesRequestSeq.current) return;
7983
setItems(res.items);
8084
setMetrics(res.metrics);
8185
setHasMore(res.items.length >= 50);
8286
setLoading(false);
8387
})
8488
.catch((e) => {
89+
if (cancelled || requestSeq !== tracesRequestSeq.current) return;
8590
setErr(String(e));
8691
setLoading(false);
8792
});
93+
return () => {
94+
cancelled = true;
95+
};
8896
}, [domainFilter, hostFilter, query]);
8997

9098
useEffect(() => {
@@ -126,18 +134,21 @@ export default function Traces() {
126134

127135
const loadMore = () => {
128136
if (loading || !hasMore) return;
137+
const requestSeq = tracesRequestSeq.current;
129138
setLoading(true);
130139
const nextOffset = (page + 1) * 50;
131140
api
132141
.traces(50, nextOffset, domainFilter, hostFilter, query)
133142
.then((res) => {
143+
if (requestSeq !== tracesRequestSeq.current) return;
134144
setItems((prev) => (prev ? [...prev, ...res.items] : res.items));
135145
setMetrics(res.metrics);
136146
setHasMore(res.items.length >= 50);
137147
setPage((p) => p + 1);
138148
setLoading(false);
139149
})
140150
.catch((e) => {
151+
if (requestSeq !== tracesRequestSeq.current) return;
141152
setErr(String(e));
142153
setLoading(false);
143154
});
@@ -296,6 +307,7 @@ export default function Traces() {
296307
<TraceCard
297308
key={t.id}
298309
trace={t}
310+
searchQuery={query}
299311
isExpanded={expandedId === t.id}
300312
onToggle={() => toggleExpanded(t.id)}
301313
onOpenInspector={() => openInspector(t)}
@@ -337,11 +349,13 @@ export default function Traces() {
337349

338350
function TraceCard({
339351
trace,
352+
searchQuery,
340353
isExpanded,
341354
onToggle,
342355
onOpenInspector,
343356
}: {
344357
trace: Trace;
358+
searchQuery: string;
345359
isExpanded: boolean;
346360
onToggle: () => void;
347361
onOpenInspector: () => void;
@@ -393,7 +407,7 @@ function TraceCard({
393407
{trace.task}
394408
</p>
395409
{trace.snippets && trace.snippets.length > 0 && (
396-
<TraceSearchHits snippets={trace.snippets} />
410+
<TraceSearchHits snippets={trace.snippets} query={searchQuery} />
397411
)}
398412
<div className="flex items-center gap-3 text-[10px] text-neutral-500 font-mono">
399413
<span>Session: {trace.session_id}</span>
@@ -415,10 +429,21 @@ function TraceCard({
415429
);
416430
}
417431

418-
function TraceSearchHits({ snippets }: { snippets: string[] }) {
432+
function TraceSearchHits({
433+
snippets,
434+
query,
435+
}: {
436+
snippets: string[];
437+
query: string;
438+
}) {
439+
const matchingSnippets = snippets.filter((snippet) =>
440+
snippetMatchesSearchQuery(snippet, query)
441+
);
442+
if (matchingSnippets.length === 0) return null;
443+
419444
return (
420445
<div className="mb-2 mt-2 space-y-1.5">
421-
{snippets.slice(0, 4).map((snippet, index) => (
446+
{matchingSnippets.slice(0, 4).map((snippet, index) => (
422447
<div
423448
key={`${snippet}-${index}`}
424449
className="border border-amber-900/30 bg-amber-950/10 px-2.5 py-1.5 text-[11px] leading-relaxed text-neutral-300"
@@ -430,6 +455,28 @@ function TraceSearchHits({ snippets }: { snippets: string[] }) {
430455
);
431456
}
432457

458+
function searchTerms(query: string): string[] {
459+
const terms = Array.from(query.matchAll(/"([^"]+)"|(\S+)/g))
460+
.map((match) => (match[1] || match[2] || "").trim().toLowerCase())
461+
.flatMap((term) => term.split(/[^0-9a-z_]+/i))
462+
.filter(Boolean);
463+
return [...new Set(terms)];
464+
}
465+
466+
function snippetMatchesSearchQuery(snippet: string, query: string): boolean {
467+
const terms = searchTerms(query);
468+
if (terms.length === 0) return true;
469+
470+
const markedTerms = Array.from(snippet.matchAll(/\[\[(.*?)\]\]/g))
471+
.map((match) => match[1].trim().toLowerCase())
472+
.flatMap((term) => term.split(/[^0-9a-z_]+/i))
473+
.filter(Boolean);
474+
475+
return markedTerms.some((marked) =>
476+
terms.some((term) => marked.startsWith(term) || term.startsWith(marked))
477+
);
478+
}
479+
433480
function SnippetText({ text }: { text: string }) {
434481
const compact = text.replace(/\s+/g, " ").trim();
435482
const parts = compact.split(/(\[\[.*?\]\])/g);

scripts/install.sh

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -335,37 +335,46 @@ main() {
335335
fi
336336

337337
if [[ "$ATELIER_NO_SERVICECTL" != "1" ]]; then
338-
info "Starting Atelier background service controller..."
339-
if [[ "$ATELIER_DRY_RUN" == "1" ]]; then
340-
echo "[dry-run] $ATELIER_BIN_DIR/atelier servicectl start --interval-seconds $ATELIER_SERVICECTL_INTERVAL_SECONDS --maintenance-interval-seconds $ATELIER_SERVICECTL_MAINTENANCE_INTERVAL_SECONDS"
341-
else
342-
"$ATELIER_BIN_DIR/atelier" servicectl start \
343-
--interval-seconds "$ATELIER_SERVICECTL_INTERVAL_SECONDS" \
344-
--maintenance-interval-seconds "$ATELIER_SERVICECTL_MAINTENANCE_INTERVAL_SECONDS" >/dev/null
345-
fi
346-
else
347-
info "Skipping background service controller because ATELIER_NO_SERVICECTL=1"
348-
fi
338+
if command -v systemctl >/dev/null 2>&1 || [[ "$(uname -s)" == "Darwin" ]]; then
339+
info "Registering Atelier services with background manager..."
340+
local background_args=()
341+
if [[ "$ATELIER_NO_STACK" != "1" && $(command -v docker) ]]; then
342+
background_args+=("--with-stack")
343+
fi
349344

350-
STACK_STARTED=0
351-
if [[ "$ATELIER_NO_STACK" != "1" ]]; then
352-
if command -v docker >/dev/null 2>&1; then
353-
info "Starting Atelier visualization stack (service + frontend)..."
354345
if [[ "$ATELIER_DRY_RUN" == "1" ]]; then
355-
echo "[dry-run] $ATELIER_BIN_DIR/atelier stack start"
346+
echo "[dry-run] $ATELIER_BIN_DIR/atelier background install ${background_args[*]}"
356347
else
357-
"$ATELIER_BIN_DIR/atelier" stack start \
358-
&& STACK_STARTED=1 \
359-
|| warn "Visualization stack did not start (Docker daemon may not be running)"
348+
"$ATELIER_BIN_DIR/atelier" background install "${background_args[@]}" >/dev/null
360349
fi
361350
else
362-
info "Skipping visualization stack because Docker is not installed"
351+
info "Starting Atelier background service controller (loose process)..."
352+
if [[ "$ATELIER_DRY_RUN" == "1" ]]; then
353+
echo "[dry-run] $ATELIER_BIN_DIR/atelier servicectl start --interval-seconds $ATELIER_SERVICECTL_INTERVAL_SECONDS --maintenance-interval-seconds $ATELIER_SERVICECTL_MAINTENANCE_INTERVAL_SECONDS"
354+
else
355+
"$ATELIER_BIN_DIR/atelier" servicectl start \
356+
--interval-seconds "$ATELIER_SERVICECTL_INTERVAL_SECONDS" \
357+
--maintenance-interval-seconds "$ATELIER_SERVICECTL_MAINTENANCE_INTERVAL_SECONDS" >/dev/null
358+
fi
359+
360+
if [[ "$ATELIER_NO_STACK" != "1" ]]; then
361+
if command -v docker >/dev/null 2>&1; then
362+
info "Starting Atelier visualization stack (service + frontend)..."
363+
if [[ "$ATELIER_DRY_RUN" == "1" ]]; then
364+
echo "[dry-run] $ATELIER_BIN_DIR/atelier stack start"
365+
else
366+
"$ATELIER_BIN_DIR/atelier" stack start \
367+
&& STACK_STARTED=1 \
368+
|| warn "Visualization stack did not start (Docker daemon may not be running)"
369+
fi
370+
fi
371+
fi
363372
fi
364373
else
365-
info "Skipping visualization stack because ATELIER_NO_STACK=1"
374+
info "Skipping background services because ATELIER_NO_SERVICECTL=1"
366375
fi
367376

368-
if [[ "$STACK_STARTED" == "1" ]]; then
377+
if [[ "$STACK_STARTED" == "1" || ( "$ATELIER_NO_SERVICECTL" != "1" && $(command -v systemctl) && "$ATELIER_NO_STACK" != "1" ) ]]; then
369378
echo " Visualization stack is running:"
370379
echo " frontend: http://localhost:3125"
371380
echo " service: http://localhost:8787"
@@ -374,7 +383,7 @@ main() {
374383
echo " Commands:"
375384
echo " atelier --version - Check core CLI version"
376385
echo " atelier-mcp --version - Check MCP server version"
377-
echo " atelier servicectl status - View background service and systemctl status"
386+
echo " atelier background status - View background service status"
378387
echo " atelier stack start - Start production API and Frontend (requires Docker)"
379388
echo " atelier stack stop - Stop the visualization stack"
380389
echo " atelier stack logs - View stack logs"

0 commit comments

Comments
 (0)