diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
index 9bbe3feef7..9cc20f758c 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
@@ -155,7 +155,10 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
{groups.map((group) => {
- const profileAgent = pickProfileAgent(group.agents);
+ const profileAgent = pickProfileAgent(
+ group.agents,
+ group.persona.displayName,
+ );
return (
(
@@ -272,7 +275,7 @@ function AgentPersonaCard({
onStartAgent: (pubkey: string) => void;
onStartPersona: (persona: AgentPersona) => void;
}) {
- const title = persona.displayName;
+ const title = agent?.name.trim() || persona.displayName;
const modelLabel = resolveAgentCardModelLabel({
agent,
personaModel: persona.model,
diff --git a/desktop/src/features/agents/ui/unifiedAgentGroups.test.mjs b/desktop/src/features/agents/ui/unifiedAgentGroups.test.mjs
new file mode 100644
index 0000000000..b44d5ccff9
--- /dev/null
+++ b/desktop/src/features/agents/ui/unifiedAgentGroups.test.mjs
@@ -0,0 +1,30 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { pickProfileAgent } from "./unifiedAgentGroups.ts";
+
+function agent(name, status = "stopped") {
+ return {
+ name,
+ status,
+ pid: status === "running" ? 123 : null,
+ };
+}
+
+test("pickProfileAgent prefers a configured identity over the persona placeholder", () => {
+ const selected = pickProfileAgent(
+ [agent("Bumble"), agent("Maria")],
+ "Bumble",
+ );
+
+ assert.equal(selected.name, "Maria");
+});
+
+test("pickProfileAgent still prefers the active runtime", () => {
+ const selected = pickProfileAgent(
+ [agent("Bumble", "running"), agent("Maria")],
+ "Bumble",
+ );
+
+ assert.equal(selected.name, "Bumble");
+});
diff --git a/desktop/src/features/agents/ui/unifiedAgentGroups.ts b/desktop/src/features/agents/ui/unifiedAgentGroups.ts
index a2d1b987f3..b61e525a8e 100644
--- a/desktop/src/features/agents/ui/unifiedAgentGroups.ts
+++ b/desktop/src/features/agents/ui/unifiedAgentGroups.ts
@@ -34,11 +34,29 @@ export function buildUnifiedGroups(
return { groups, ungrouped, unknown };
}
-export function pickProfileAgent(agents: ManagedAgent[]) {
+export function pickProfileAgent(
+ agents: ManagedAgent[],
+ personaDisplayName?: string,
+) {
+ const placeholderName = personaDisplayName?.trim().toLocaleLowerCase();
return [...agents].sort((left, right) => {
const activeDiff =
Number(isManagedAgentActive(right)) - Number(isManagedAgentActive(left));
if (activeDiff !== 0) return activeDiff;
+
+ // A persona can have both its starter placeholder (for example Bumble)
+ // and a real configured runtime (for example Maria). Prefer the configured
+ // identity so the unified card does not hide the agent operators recognise.
+ if (placeholderName) {
+ const leftIsPlaceholder =
+ left.name.trim().toLocaleLowerCase() === placeholderName;
+ const rightIsPlaceholder =
+ right.name.trim().toLocaleLowerCase() === placeholderName;
+ const placeholderDiff =
+ Number(leftIsPlaceholder) - Number(rightIsPlaceholder);
+ if (placeholderDiff !== 0) return placeholderDiff;
+ }
+
return left.name.localeCompare(right.name);
})[0];
}
diff --git a/desktop/src/features/cos-running-order/lib/cosDeliveryRoom.test.mjs b/desktop/src/features/cos-running-order/lib/cosDeliveryRoom.test.mjs
index 4dc5a4ff19..f9d53d08d3 100644
--- a/desktop/src/features/cos-running-order/lib/cosDeliveryRoom.test.mjs
+++ b/desktop/src/features/cos-running-order/lib/cosDeliveryRoom.test.mjs
@@ -320,6 +320,21 @@ test("expires the projection at the earliest source or presented evidence deadli
}
});
+test("ignores invalid evidence when calculating the projection expiry", () => {
+ const room = projectCosDeliveryRoom(envelope(), { now: NOW });
+ room.deliveryRoom.workItems[0].evidence.push({
+ ...evidence("unavailable-claim", "unknown"),
+ observedAt: "",
+ freshness: "invalid",
+ freshForMs: 7 * 24 * 60 * 60 * 1_000,
+ });
+
+ assert.equal(
+ cosDeliveryRoomExpiresAt(room),
+ new Date("2026-07-31T09:19:00.000Z").getTime(),
+ );
+});
+
test("rejects unbounded or unsafe lifetimes and accepts the reviewed boundaries", () => {
const hugeSourceLifetime = copy(envelope());
hugeSourceLifetime.source.maxAgeSeconds = 1e308;
diff --git a/desktop/src/features/cos-running-order/lib/cosDeliveryRoomExpiry.ts b/desktop/src/features/cos-running-order/lib/cosDeliveryRoomExpiry.ts
index 036213717c..8c4a62b3c3 100644
--- a/desktop/src/features/cos-running-order/lib/cosDeliveryRoomExpiry.ts
+++ b/desktop/src/features/cos-running-order/lib/cosDeliveryRoomExpiry.ts
@@ -116,6 +116,11 @@ export function cosDeliveryRoomExpiresAt(room: CosDeliveryRoom): number {
expiresAt(room.source.agentHealth.observedAt, sourceLifetimeMs),
];
const addEvidence = (evidence: DeliveryRoomEvidence) => {
+ // Invalid evidence is deliberately retained for honest unavailable/unknown
+ // claims, but it has no verifiable timestamp and therefore cannot define a
+ // semantic expiry deadline. The parser has already checked that the
+ // declared freshness agrees with the missing or malformed timestamp.
+ if (evidence.freshness === "invalid") return;
deadlines.push(expiresAt(evidence.observedAt, evidence.freshForMs));
};
diff --git a/mobile/lib/features/cos_running_order/cos_running_order.dart b/mobile/lib/features/cos_running_order/cos_running_order.dart
index fcdd282021..81f16df05d 100644
--- a/mobile/lib/features/cos_running_order/cos_running_order.dart
+++ b/mobile/lib/features/cos_running_order/cos_running_order.dart
@@ -1,110 +1,117 @@
enum CosRunningOrderState {
- blocked,
- humanTest,
- running,
- active,
ready,
- queued,
+ building,
+ independentReview,
+ stagingVerification,
completed,
}
class CosRunningOrderCounts {
- final int active;
- final int blocked;
- final int completed;
- final int humanTest;
- final int queued;
final int ready;
- final int running;
+ final int building;
+ final int independentReview;
+ final int stagingVerification;
+ final int completed;
+ final int needsManager;
+ final int blockedOrStalled;
const CosRunningOrderCounts({
- required this.active,
- required this.blocked,
- required this.completed,
- required this.humanTest,
- required this.queued,
required this.ready,
- required this.running,
+ required this.building,
+ required this.independentReview,
+ required this.stagingVerification,
+ required this.completed,
+ required this.needsManager,
+ required this.blockedOrStalled,
});
-
- factory CosRunningOrderCounts.fromJson(Map json) {
- return CosRunningOrderCounts(
- active: _integer(json['active']),
- blocked: _integer(json['blocked']),
- completed: _integer(json['completed']),
- humanTest: _integer(json['human_test']),
- queued: _integer(json['queued']),
- ready: _integer(json['ready']),
- running: _integer(json['agent_running'] ?? json['running']),
- );
- }
}
class CosRunningOrderItem {
final String key;
- final String summary;
- final String jiraStatus;
- final String priority;
+ final String title;
+ final String currentActivity;
+ final String nextAction;
+ final String owner;
final CosRunningOrderState state;
- final List blockers;
- final bool stagingEvidenced;
+ final String health;
const CosRunningOrderItem({
required this.key,
- required this.summary,
- required this.jiraStatus,
- required this.priority,
+ required this.title,
+ required this.currentActivity,
+ required this.nextAction,
+ required this.owner,
required this.state,
- required this.blockers,
- required this.stagingEvidenced,
+ required this.health,
});
factory CosRunningOrderItem.fromJson(Map json) {
+ final reference = _map(json['externalReference']);
+ final owner = _map(json['owner']);
return CosRunningOrderItem(
- key: _text(json['key']),
- summary: _text(json['summary']),
- jiraStatus: _text(json['jira_status']),
- priority: _text(json['priority']),
- state: _state(json['execution_state'] ?? json['state']),
- blockers: _strings(json['blockers']),
- stagingEvidenced: json['staging_evidenced'] == true,
+ key: _text(reference['key']).isEmpty
+ ? _text(json['id'])
+ : _text(reference['key']),
+ title: _text(json['title']),
+ currentActivity: _text(json['currentActivity']),
+ nextAction: _text(json['nextAction']),
+ owner: _text(owner['label']),
+ state: _state(json['stage']),
+ health: _text(json['health']),
);
}
}
class CosRunningOrderSnapshot {
final DateTime? generatedAt;
- final String operationalStatus;
- final String overallStatus;
- final String? stagingRevision;
+ final String sourceStatus;
final CosRunningOrderCounts counts;
final List items;
const CosRunningOrderSnapshot({
required this.generatedAt,
- required this.operationalStatus,
- required this.overallStatus,
- required this.stagingRevision,
+ required this.sourceStatus,
required this.counts,
required this.items,
});
factory CosRunningOrderSnapshot.fromJson(Map json) {
- if (json['schema'] != 'mac-workspace/cos-running-order/v1') {
- throw const FormatException('Unsupported COS running-order snapshot');
+ if (json['schemaVersion'] != 'mac-workspace/delivery-room/v1' ||
+ json['readOnly'] != true) {
+ throw const FormatException('Unsupported Delivery Room snapshot');
+ }
+ final source = _map(json['source']);
+ if (source['status'] != 'fresh') {
+ throw const FormatException('Delivery Room evidence is not current');
}
+ final room = _map(json['deliveryRoom']);
+ if (room['schemaVersion'] != 'delivery-room-projection/v1') {
+ throw const FormatException('Unsupported Delivery Room projection');
+ }
+ final items = _maps(room['workItems'])
+ .map(CosRunningOrderItem.fromJson)
+ .where((item) => item.key.isNotEmpty)
+ .toList();
+ int count(CosRunningOrderState state) =>
+ items.where((item) => item.state == state).length;
+ final attention = _map(room['attention']);
return CosRunningOrderSnapshot(
- generatedAt: DateTime.tryParse(_text(json['generated_at_utc'])),
- operationalStatus: _text(json['operational_status']),
- overallStatus: _text(json['overall_status']),
- stagingRevision: _text(json['staging_revision']).isEmpty
- ? null
- : _text(json['staging_revision']),
- counts: CosRunningOrderCounts.fromJson(_map(json['counts'])),
- items: _maps(json['items'])
- .map(CosRunningOrderItem.fromJson)
- .where((item) => item.key.isNotEmpty)
- .toList(),
+ generatedAt: DateTime.tryParse(_text(json['generatedAt'])),
+ sourceStatus: _text(source['status']),
+ counts: CosRunningOrderCounts(
+ ready: count(CosRunningOrderState.ready),
+ building: count(CosRunningOrderState.building),
+ independentReview: count(CosRunningOrderState.independentReview),
+ stagingVerification: count(CosRunningOrderState.stagingVerification),
+ completed: count(CosRunningOrderState.completed),
+ needsManager: _strings(
+ _map(attention['needsManager'])['workItemIds'],
+ ).length,
+ blockedOrStalled: _strings(
+ _map(attention['blockedOrStalled'])['workItemIds'],
+ ).length,
+ ),
+ items: items,
);
}
}
@@ -120,37 +127,28 @@ Uri cosRunningOrderUri(String relayUrl) {
};
return relay.replace(
scheme: scheme,
- path: '/api/cos-running-order/v1',
+ path: '/api/mac-delivery-room/v1',
query: null,
fragment: null,
);
}
-Map _map(dynamic value) {
- return value is Map ? Map.from(value) : {};
-}
+Map _map(dynamic value) =>
+ value is Map ? Map.from(value) : {};
-List