Skip to content
Open
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
7 changes: 5 additions & 2 deletions desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,10 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
<div className="space-y-3" data-testid="unified-agents-groups">
<div className={AGENT_CARD_GRID_CLASS}>
{groups.map((group) => {
const profileAgent = pickProfileAgent(group.agents);
const profileAgent = pickProfileAgent(
group.agents,
group.persona.displayName,
);
return (
<AgentPersonaCard
actions={(effectiveAvatarUrl, isEffectiveAvatarLoading) => (
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions desktop/src/features/agents/ui/unifiedAgentGroups.test.mjs
Original file line number Diff line number Diff line change
@@ -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");
});
20 changes: 19 additions & 1 deletion desktop/src/features/agents/ui/unifiedAgentGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};

Expand Down
176 changes: 87 additions & 89 deletions mobile/lib/features/cos_running_order/cos_running_order.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> 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<String> 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<String, dynamic> 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']),
Comment on lines +55 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject delivery states unsupported by evidence

Validate each projected stage and health claim against its gates and current evidence before constructing the item. Currently the mobile parser does not read objectiveGates or evidence and accepts arbitrary health strings, so a response can claim complete without passed gates or current verification, or needs_manager without any supporting evidence, and the mobile UI will present that contradictory claim as delivery truth.

Useful? React with 👍 / 👎.

);
}
}

class CosRunningOrderSnapshot {
final DateTime? generatedAt;
final String operationalStatus;
final String overallStatus;
final String? stagingRevision;
final String sourceStatus;
final CosRunningOrderCounts counts;
final List<CosRunningOrderItem> 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<String, dynamic> 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');
Comment on lines +79 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify the Delivery Room generation digest

Reject the response unless generationId is present, well-formed, and matches the canonical envelope digest. As written, any payload with the expected schema, readOnly: true, and source.status: "fresh" is accepted even when generationId is missing or mismatched, so a partially written or altered projection can be presented as trusted delivery data; the desktop loader performs this verification in verifyCosDeliveryRoomGeneration before projecting the response.

Useful? React with 👍 / 👎.

}
final source = _map(json['source']);
if (source['status'] != 'fresh') {
throw const FormatException('Delivery Room evidence is not current');
Comment on lines +83 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Expire evidence using its timestamps

Recompute freshness from generatedAt, source.maxAgeSeconds, source observations, and displayed evidence rather than trusting the serialized source.status. A payload fetched while fresh remains cached indefinitely because this notifier has no periodic refresh and the page retains its last snapshot, so leaving Delivery Room open past its evidence deadline continues to show “Evidence current”; an already-expired response from an intermediary cache is likewise accepted.

Useful? React with 👍 / 👎.

}
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,
);
}
}
Expand All @@ -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<String, dynamic> _map(dynamic value) {
return value is Map ? Map<String, dynamic>.from(value) : {};
}
Map<String, dynamic> _map(dynamic value) =>
value is Map ? Map<String, dynamic>.from(value) : {};

List<Map<String, dynamic>> _maps(dynamic value) {
return value is List ? value.map(_map).toList() : [];
}
List<Map<String, dynamic>> _maps(dynamic value) =>
value is List ? value.map(_map).toList() : [];

List<String> _strings(dynamic value) {
return value is List ? value.whereType<String>().toList() : [];
}
List<String> _strings(dynamic value) =>
value is List ? value.whereType<String>().toList() : [];

String _text(dynamic value) => value is String ? value : '';

int _integer(dynamic value) => value is int ? value : 0;

CosRunningOrderState _state(dynamic value) {
return switch (value) {
'blocked' => CosRunningOrderState.blocked,
'human-test' => CosRunningOrderState.humanTest,
'running' => CosRunningOrderState.running,
'active' => CosRunningOrderState.active,
'ready' => CosRunningOrderState.ready,
'queued' => CosRunningOrderState.queued,
'completed' => CosRunningOrderState.completed,
_ => throw const FormatException('Unsupported COS running-order state'),
};
}
CosRunningOrderState _state(dynamic value) => switch (value) {
'ready' => CosRunningOrderState.ready,
'building' => CosRunningOrderState.building,
'independent_review' => CosRunningOrderState.independentReview,
'staging_verification' => CosRunningOrderState.stagingVerification,
'complete' => CosRunningOrderState.completed,
_ => throw const FormatException('Unsupported Delivery Room stage'),
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ part 'cos_running_order_page/body.dart';

enum _RunningOrderFilter {
focus,
blocked,
running,
active,
needsManager,
ready,
humanTest,
queued,
building,
review,
verification,
complete,
}

class CosRunningOrderPage extends StatelessWidget {
const CosRunningOrderPage({super.key});

@override
Widget build(BuildContext context) => const CosWorkspaceModuleGate(
title: 'COS Running Order',
title: 'Delivery Room',
requiredModules: ['running_order'],
child: _AuthorisedCosRunningOrderPage(),
);
Expand Down Expand Up @@ -64,7 +64,7 @@ class _AuthorisedCosRunningOrderPage extends HookConsumerWidget {
}

return FrostedScaffold(
appBar: const FrostedAppBar(title: Text('COS Running Order')),
appBar: const FrostedAppBar(title: Text('Delivery Room')),
body: SafeArea(
top: false,
child: Padding(
Expand Down
Loading
Loading