Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions MISSION-CONTROL-TEMP-TRACKER.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sprintDate": "2026-02-23",
"block": "5/5",
"updatedAt": "2026-03-02T07:35:00Z",
"updatedAt": "2026-03-03T08:05:00Z",
"pooAppAgentApiTracking": {
"attempted": true,
"status": "blocked",
Expand Down Expand Up @@ -46,31 +46,43 @@
{
"id": "MC-P1-PR-OPEN",
"title": "Open/update PR with overnight mission control scope summary",
"status": "pending",
"artifacts": []
"status": "done",
"artifacts": [
"https://github.com/aviarytech/todo/pull/153"
]
},
{
"id": "MC-P1-AC3-PRESENCE-WIRE",
"title": "Wire list-level presence indicator + heartbeat and unskip AC3 feature gate",
"status": "done",
"artifacts": [
"src/pages/ListView.tsx",
"e2e/mission-control-phase1.spec.ts"
]
}
],
"validation": {
"playwrightSpecRun": "partial",
"command": "npm run test:e2e -- e2e/mission-control-phase1.spec.ts",
"command": "npm run test:e2e -- e2e/mission-control-phase1.spec.ts -g \"AC3 presence freshness\"",
"result": {
"passed": 1,
"skipped": 6,
"passed": 0,
"skipped": 1,
"failed": 0
},
"observabilityValidation": {
"command": "npm run mission-control:validate-observability",
"passed": true
},
"notes": [
"Seeded local auth fixture added for OTP-gated routes; baseline harness remains runnable.",
"AC1/AC2/AC3/AC5b remain conditionally skipped when assignee/activity/presence UI surfaces are absent in current build.",
"Perf harness supports production-sized fixture path via MISSION_CONTROL_FIXTURE_PATH."
"Added quick Assign action in list item UI wired to items.updateItem(assigneeDid=userDid).",
"Removed AC1 feature-availability dynamic skip; AC1 now asserts Assign control visibility.",
"Remaining AC1 skip is environment readiness gate (authenticated app shell availability).",
"AC3 feature dynamic skip removed; scenario still environment-gated on authenticated app-shell readiness."
]
},
"next": [
"Wire assignee/activity/presence UI+backend then remove dynamic skips",
"Run production-sized perf profile: MISSION_CONTROL_FIXTURE_PATH=e2e/fixtures/mission-control.production.json npm run test:e2e -- e2e/mission-control-phase1.spec.ts",
"Open PR with this P0-3/P0-4 delta and CI artifacts"
"Acquire stable authenticated e2e backend session so AC3 can execute instead of setup-skip",
"Run full mission-control-phase1 spec on production-sized fixture to capture AC5 metrics without skips",
"Close MC-P1-TRACKING-AUTH blocker once agent API credentials/session are provisioned"
]
}
}
11 changes: 11 additions & 0 deletions convex/agentTeam.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { emitServerMetric } from "./lib/observability";

type AgentStatus = "idle" | "working" | "error";

Expand Down Expand Up @@ -173,6 +174,16 @@ export const getRunHealth = query({
return now - agent.lastStatusAt >= criticalThresholdMs;
});

for (const agent of active) {
const heartbeatAgeMs = agent.lastHeartbeatAt ? Math.max(0, now - agent.lastHeartbeatAt) : criticalThresholdMs;
emitServerMetric("agent_heartbeat_age_ms", "gauge", heartbeatAgeMs, {
agentSlug: agent.agentSlug,
});
}

const staleCount = staleAgents.filter((a) => a.isStale).length;
emitServerMetric("agent_stale_total", "gauge", staleCount);

return {
updatedAt: now,
totals: {
Expand Down
17 changes: 15 additions & 2 deletions convex/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { v } from "convex/values";
import type { Id } from "./_generated/dataModel";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { mutation, query } from "./_generated/server";
import { insertActivityEvent } from "./lib/activityEvents";

/**
* Helper to check if a user can view a list.
Expand Down Expand Up @@ -116,12 +117,24 @@ export const addComment = mutation({
throw new Error("Not authorized to comment on this item");
}

return await ctx.db.insert("comments", {
const createdAt = Date.now();
const commentId = await ctx.db.insert("comments", {
itemId: args.itemId,
userDid: args.userDid,
text: args.text.trim(),
createdAt: Date.now(),
createdAt,
});

await insertActivityEvent(ctx, {
listId: item.listId,
itemId: args.itemId,
eventType: "commented",
actorDid: args.userDid,
metadata: { commentId, textPreview: args.text.trim().slice(0, 120) },
createdAt,
});

return commentId;
},
});

Expand Down
41 changes: 41 additions & 0 deletions convex/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Id } from "./_generated/dataModel";
import { mutation, query } from "./_generated/server";
import { withMutationObservability } from "./lib/observability";
import { canUserEditList } from "./lib/permissions";
import { insertActivityEvent } from "./lib/activityEvents";

/**
* Creates a Verifiable Credential for item authorship (creation).
Expand Down Expand Up @@ -206,6 +207,14 @@ export const addItem = mutation({
// Store the VC proof on the item
await ctx.db.patch(itemId, { vcProofs: [authorshipVC] });

await insertActivityEvent(ctx, {
listId: args.listId,
itemId,
eventType: "created",
actorDid: args.createdByDid,
createdAt: now,
});

return itemId;
}),
});
Expand Down Expand Up @@ -272,7 +281,31 @@ export const updateItem = mutation({
if (args.clearAssigneeDid) updates.assigneeDid = undefined;
if (args.clearGroceryAisle) updates.groceryAisle = undefined;

const assigneeChanged = Object.prototype.hasOwnProperty.call(updates, "assigneeDid") && updates.assigneeDid !== item.assigneeDid;
const editedKeys = Object.keys(updates).filter((key) => key !== "updatedAt" && key !== "assigneeDid");

await ctx.db.patch(args.itemId, updates);

if (assigneeChanged) {
await insertActivityEvent(ctx, {
listId: item.listId,
itemId: args.itemId,
eventType: "assigned",
actorDid: args.userDid,
assigneeDid: (updates.assigneeDid as string | undefined) ?? undefined,
});
}

if (editedKeys.length > 0) {
await insertActivityEvent(ctx, {
listId: item.listId,
itemId: args.itemId,
eventType: "edited",
actorDid: args.userDid,
metadata: { fields: editedKeys },
});
}

return args.itemId;
}),
});
Expand Down Expand Up @@ -358,6 +391,14 @@ export const checkItem = mutation({
vcProofs: updatedProofs,
});

await insertActivityEvent(ctx, {
listId: item.listId,
itemId: args.itemId,
eventType: "completed",
actorDid: args.checkedByDid,
createdAt: args.checkedAt,
});

// If item has recurrence, create a new unchecked copy with next due date
if (item.recurrence) {
const nextDueDate = calculateNextDueDate(
Expand Down
29 changes: 29 additions & 0 deletions convex/lib/activityEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { MutationCtx } from "../_generated/server";
import type { Id } from "../_generated/dataModel";

export type ActivityEventType = "created" | "completed" | "uncompleted" | "assigned" | "commented" | "edited";

export async function insertActivityEvent(
ctx: MutationCtx,
args: {
listId: Id<"lists">;
itemId?: Id<"items">;
eventType: ActivityEventType;
actorDid: string;
assigneeDid?: string;
metadata?: Record<string, unknown>;
createdAt?: number;
}
) {
const createdAt = args.createdAt ?? Date.now();

await ctx.db.insert("activityEvents", {
listId: args.listId,
itemId: args.itemId,
eventType: args.eventType,
actorDid: args.actorDid,
assigneeDid: args.assigneeDid,
metadata: args.metadata ? JSON.stringify(args.metadata) : undefined,
createdAt,
});
}
6 changes: 6 additions & 0 deletions convex/missionControl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
import { emitServerMetric } from "./lib/observability";

const PRESENCE_TTL_MS = 90_000;

Expand Down Expand Up @@ -46,6 +47,11 @@ export const setItemAssignee = mutation({
createdAt: Date.now(),
});

emitServerMetric("activity_event_total", "counter", 1, {
action: "assigned",
listId: item.listId,
});

return { ok: true };
},
});
Expand Down
40 changes: 28 additions & 12 deletions convex/missionControlCore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
import { emitServerMetric } from "./lib/observability";

async function hasListAccess(ctx: any, listId: Id<"lists">, userDid: string) {
const list = await ctx.db.get(listId);
Expand Down Expand Up @@ -951,20 +952,35 @@ export const controlAgentLaunch = mutation({
patch.archivedAt = undefined;
}

await ctx.db.patch(profile._id, patch as any);
try {
await ctx.db.patch(profile._id, patch as any);

await ctx.db.insert("agentControlEvents", {
ownerDid: args.ownerDid,
actorDid: args.actorDid,
agentProfileId: profile._id,
agentSlug: args.agentSlug,
action: args.action,
targetAgentSlug: args.targetAgentSlug,
reason: args.reason,
createdAt: now,
});
await ctx.db.insert("agentControlEvents", {
ownerDid: args.ownerDid,
actorDid: args.actorDid,
agentProfileId: profile._id,
agentSlug: args.agentSlug,
action: args.action,
targetAgentSlug: args.targetAgentSlug,
reason: args.reason,
createdAt: now,
});

emitServerMetric("run_control_action_total", "counter", 1, {
action: args.action,
result: "ok",
agentSlug: args.agentSlug,
});

return { ok: true, agentId: profile._id, action: args.action as LaunchAction };
return { ok: true, agentId: profile._id, action: args.action as LaunchAction };
} catch (error) {
emitServerMetric("run_control_action_total", "counter", 1, {
action: args.action,
result: "failed",
agentSlug: args.agentSlug,
});
throw error;
}
},
});

Expand Down
12 changes: 10 additions & 2 deletions docs/mission-control/phase1-observability-alert-routing.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@
"routing": {
"staging": {
"channel": "slack://aviary-mission-control-dev",
"escalation": "none"
"escalation": "none",
"acknowledgement": {
"required": false,
"incidentNoteRequired": false
}
},
"production": {
"channel": "slack://aviary-oncall-mission-control",
"pager": "pagerduty://mission-control-primary",
"escalation": "15m"
"escalation": "15m",
"acknowledgement": {
"required": true,
"incidentNoteRequired": true
}
}
},
"alerts": [
Expand Down
20 changes: 20 additions & 0 deletions docs/mission-control/phase1-observability-dashboard-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
],
"unit": "%"
},
{
"metric": "mutation_latency_ms",
"view": [
"p50",
"p95"
],
"unit": "ms",
"groupBy": [
"mutationName"
]
},
{
"metric": "active_presence_sessions",
"view": [
Expand Down Expand Up @@ -151,6 +162,15 @@
"groupBy": [
"route"
]
},
{
"metric": "route_view_total",
"view": [
"rate_5m"
],
"groupBy": [
"route"
]
}
]
}
Expand Down
8 changes: 4 additions & 4 deletions docs/mission-control/phase1-observability-metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"action",
"env"
],
"status": "planned"
"status": "implemented"
},
{
"name": "invalid_assignee_reference_total",
Expand Down Expand Up @@ -125,15 +125,15 @@
"agentSlug",
"env"
],
"status": "planned"
"status": "implemented"
},
{
"name": "agent_stale_total",
"type": "gauge",
"dimensions": [
"env"
],
"status": "planned"
"status": "implemented"
},
{
"name": "run_control_action_total",
Expand All @@ -143,7 +143,7 @@
"result",
"env"
],
"status": "planned"
"status": "implemented"
}
],
"alerts": [
Expand Down
Loading
Loading