Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
e79def7
Add notification feed chronology regression coverage
azooz2003-bit Jul 11, 2026
da2f0f7
Preserve complete chronological notification history
azooz2003-bit Jul 11, 2026
832b5c9
Fix notification review regressions
azooz2003-bit Jul 11, 2026
093280f
Clear stale read indicator on restore
azooz2003-bit Jul 11, 2026
9337ae7
Preserve newest notification banner
azooz2003-bit Jul 11, 2026
305de95
Bound notification queue and banner supersession
azooz2003-bit Jul 11, 2026
7d54596
Preserve notifications across queue pressure
azooz2003-bit Jul 11, 2026
aedd812
Test duplicate notification restore replay
azooz2003-bit Jul 11, 2026
1792614
Preserve canonical notification on restore replay
azooz2003-bit Jul 11, 2026
f6d63a7
Stabilize notification queue assertions
azooz2003-bit Jul 11, 2026
f71b041
Bound notification backpressure admission
azooz2003-bit Jul 11, 2026
c8abcf4
Keep notification backpressure off main actor
azooz2003-bit Jul 11, 2026
2200c7b
Transfer notifications across session replacement
azooz2003-bit Jul 12, 2026
902e20f
Migrate queued notification ownership safely
Jul 13, 2026
544a1ae
Merge remote-tracking branch 'origin/main' into task-notification-chr…
Jul 13, 2026
9f847e5
Keep notification delivery within controller budget
Jul 13, 2026
3f0526f
Harden notification delivery boundaries
Jul 13, 2026
f8c6097
Fit notification store within CI budget
Jul 13, 2026
fabe81f
Test restored notification banner ownership
Jul 13, 2026
b703044
Preserve notification banner ownership across restore
Jul 13, 2026
a60103b
Test unmapped notification session transfers
Jul 13, 2026
ff1b867
Retain unmapped notification session identities
Jul 13, 2026
550f4a6
Split notification session identity tests
Jul 13, 2026
8435ed8
Test duplicate notification identity degradation
Jul 13, 2026
d8d45f2
Harden notification admission ownership
Jul 14, 2026
e8855b2
Test effects-only notification replay idempotency
Jul 14, 2026
e0afd3a
Make notification replay effects idempotent
Jul 14, 2026
3304500
Localize notification queue saturation response
Jul 14, 2026
5295ad5
Make notification feed insertion incremental
Jul 14, 2026
8829a19
Merge origin/main into task-notification-chronological-feed
Jul 14, 2026
fe23b51
Keep merged notification files within budgets
Jul 14, 2026
38ff647
Name notification queue saturation message
Jul 14, 2026
fc41d12
Repair notification merge integration
Jul 14, 2026
2f69eae
Reconcile notification retargeting semantics
Jul 14, 2026
fc05a67
Unify notification retargeting entrypoints
Jul 14, 2026
0b32d99
Test banner ownership transfer collisions
Jul 14, 2026
7859e3e
Dismiss banner owners displaced by session transfer
Jul 14, 2026
fa0ca02
Test banner ownership rebind collisions
Jul 14, 2026
e423a08
Dismiss banner owners displaced by surface rebind
Jul 14, 2026
89c6167
Test notification location field preservation
Jul 14, 2026
b74eb30
Preserve notification fields across location changes
Jul 14, 2026
0b16a20
Verify late arrivals preserve banner ownership
Jul 14, 2026
c27c564
Merge commit '1c22c556433fe035cdc60372bdd7443613f49a92' into task-not…
Jul 14, 2026
6c78cdd
Verify notification list stays off the main actor
Jul 14, 2026
aff488d
Move notification list formatting off the main actor
Jul 14, 2026
be07814
Verify restore tie-breaker covers every payload field
Jul 14, 2026
25d46ee
Include routing fields in restore tie-breaker
Jul 14, 2026
7613d20
Test source-confined banner ownership on rebind
Jul 14, 2026
b19d4c0
Keep source-confined banner ownership on rebind
Jul 14, 2026
5d8149c
Test notification lifecycle review gaps
Jul 14, 2026
e937e46
Fix notification lifecycle review gaps
Jul 14, 2026
46fe00b
Test notification review second-pass gaps
Jul 14, 2026
a9afad6
Fix notification review second-pass gaps
Jul 14, 2026
93b123a
Test notification lifecycle replacement races
Jul 14, 2026
b4eb67e
Fix notification lifecycle replacement races
Jul 15, 2026
1e44262
Test notification lifecycle routing gaps
Jul 15, 2026
2b661e9
Fix notification lifecycle routing gaps
Jul 15, 2026
7e0f831
Test notification feed review P1 gaps
Jul 15, 2026
a6dc606
Fix notification feed review P1 gaps
azooz2003-bit Jul 15, 2026
d97f814
Stabilize notification feed cap regression test
Jul 15, 2026
9a16474
Test notification feed review repair gaps
Jul 15, 2026
654f845
Fix notification feed review repair gaps
Jul 15, 2026
8d1a491
Test notification feed review repair gaps
Jul 15, 2026
1536c51
Fix notification feed review repair gaps
Jul 15, 2026
d480569
Test notification feed autoreview regressions
Jul 15, 2026
4efea1f
Fix notification feed autoreview regressions
Jul 15, 2026
8dda068
Test notification feed route and eviction review gaps
Jul 15, 2026
15da458
Fix notification feed route and eviction review gaps
Jul 15, 2026
438de6f
Test notification feed review lifecycle gaps
Jul 15, 2026
765b582
Fix notification feed review lifecycle gaps
Jul 15, 2026
431c1de
Fix notification feed autoreview scale paths
Jul 15, 2026
33db3d6
Fix notification feed review edge cases
Jul 15, 2026
06fb33e
Bound reliable notification admission wait
Jul 15, 2026
da4dd90
Fix notification feed review edge cases
Jul 15, 2026
f2cffda
Fix notification review follow-ups
Jul 15, 2026
8cc98f4
Fix notification retry and superseded hot paths
Jul 15, 2026
34d7170
Fix notification storage and sync delivery edge cases
Jul 15, 2026
44461c2
Fix notification projection and persistence hot paths
Jul 15, 2026
2297a9c
Fix notification feed review hot paths
Jul 15, 2026
123f6b7
Merge remote-tracking branch 'origin/main' into task-notification-chr…
Jul 15, 2026
d33183d
Merge remote-tracking branch 'origin/main' into task-notification-chr…
Jul 15, 2026
d3c2175
Fix notification feed autoreview regressions
Jul 15, 2026
1f7f466
Merge remote-tracking branch 'origin/main' into task-notification-chr…
Jul 15, 2026
823d7ce
Fix notification feed review gaps
Jul 15, 2026
b857e2b
Fix notification review feedback
Jul 15, 2026
b4ea500
Fix iOS reconnect and build isolation (#8299)
azooz2003-bit Jul 17, 2026
2bd2b31
Merge main at 01cf2175dc into notification feed
Jul 17, 2026
44bf55e
Accept notification feed in sidebar index
Jul 17, 2026
666ec8b
Fix sidebar status tint closure capture
Jul 17, 2026
3f7cf4e
Align workspace fork menu with pinned bonsplit
Jul 17, 2026
a93d323
Add cooldown policy ordering regression tests
Jul 22, 2026
daf0b5e
Serialize notification cooldown policy completion
Jul 22, 2026
ffe3932
Merge main into notification feed branch
Jul 22, 2026
b9bc9b6
Fix mobile notification feed reconciliation build
Jul 22, 2026
9e36825
Fix agent resume liveness test initializer
Jul 22, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public final class ControlCommandCoordinator {
return systemIdentify(request.params, context: context)
case "system.tree":
return systemTree(request.params, context: context)
case "notification.list":
return notificationList(context: context)
case "surface.send_text":
return surfaceSendText(request.params, context: context)
case "surface.send_key":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ extension ControlCommandCoordinator {
case "notification.create_for_target":
return notificationCreateForTarget(request.params)
case "notification.list":
return notificationList()
return notificationList(context: context)
case "notification.clear":
return notificationClear()
case "notification.dismiss":
Expand Down Expand Up @@ -144,10 +144,71 @@ extension ControlCommandCoordinator {

// MARK: - List / clear

private struct NotificationPayloadRefs: Sendable {
let workspace: JSONValue
let surface: JSONValue
}

private struct NotificationListHopOutcome: Sendable {
let snapshots: [ControlNotificationSnapshot]
let refs: [NotificationPayloadRefs]
}

/// `notification.list` — every notification, with read state.
func notificationList() -> ControlCallResult {
let items = (context?.controlNotificationList() ?? []).map {
notificationPayload($0, opened: nil, includeReadState: true)
///
/// Full-list snapshotting and ref minting take one bounded main hop. Date
/// rendering and JSON row construction scale with history size, so both run
/// on the calling socket-worker thread with response encoding.
nonisolated func notificationList(
context: (any ControlCommandContext)?
) -> ControlCallResult {
guard let context else {
return .ok(.object(["notifications": .array([])]))
}

let outcome: NotificationListHopOutcome = context.controlResolveOnMain { seam in
let snapshots = seam.controlNotificationList()
var workspaceRefs: [UUID: JSONValue] = [:]
var surfaceRefs: [UUID: JSONValue] = [:]
var refs: [NotificationPayloadRefs] = []
refs.reserveCapacity(snapshots.count)
for snapshot in snapshots {
let workspaceRef: JSONValue
if let cached = workspaceRefs[snapshot.workspaceID] {
workspaceRef = cached
} else {
workspaceRef = self.ref(.workspace, snapshot.workspaceID)
workspaceRefs[snapshot.workspaceID] = workspaceRef
}

let surfaceRef: JSONValue
if let surfaceID = snapshot.surfaceID {
if let cached = surfaceRefs[surfaceID] {
surfaceRef = cached
} else {
surfaceRef = self.ref(.surface, surfaceID)
surfaceRefs[surfaceID] = surfaceRef
}
} else {
surfaceRef = .null
}
refs.append(NotificationPayloadRefs(
workspace: workspaceRef,
surface: surfaceRef
))
}
return NotificationListHopOutcome(snapshots: snapshots, refs: refs)
}

let formatter = Self.notificationTimestampFormatter()
let items = outcome.snapshots.enumerated().map { index, snapshot in
JSONValue.object(Self.notificationPayloadObject(
snapshot,
createdAtISO8601: formatter.string(from: snapshot.createdAt),
refs: outcome.refs[index],
opened: nil,
includeReadState: true
))
}
return .ok(.object(["notifications": .array(items)]))
}
Expand Down Expand Up @@ -332,18 +393,38 @@ extension ControlCommandCoordinator {
_ snapshot: ControlNotificationSnapshot,
opened: Bool?,
includeReadState: Bool
) -> [String: JSONValue] {
let formatter = Self.notificationTimestampFormatter()
return Self.notificationPayloadObject(
snapshot,
createdAtISO8601: formatter.string(from: snapshot.createdAt),
refs: NotificationPayloadRefs(
workspace: ref(.workspace, snapshot.workspaceID),
surface: ref(.surface, snapshot.surfaceID)
),
opened: opened,
includeReadState: includeReadState
)
}

private nonisolated static func notificationPayloadObject(
_ snapshot: ControlNotificationSnapshot,
createdAtISO8601: String,
refs: NotificationPayloadRefs,
opened: Bool?,
includeReadState: Bool
) -> [String: JSONValue] {
var payload: [String: JSONValue] = [
"id": .string(snapshot.id.uuidString),
"workspace_id": .string(snapshot.workspaceID.uuidString),
"workspace_ref": ref(.workspace, snapshot.workspaceID),
"surface_id": orNull(snapshot.surfaceID?.uuidString),
"surface_ref": ref(.surface, snapshot.surfaceID),
"workspace_ref": refs.workspace,
"surface_id": snapshot.surfaceID.map { .string($0.uuidString) } ?? .null,
"surface_ref": refs.surface,
"title": .string(snapshot.title),
"subtitle": .string(snapshot.subtitle),
"body": .string(snapshot.body),
"created_at": .string(snapshot.createdAtISO8601),
"tab_title": orNull(snapshot.tabTitle),
"created_at": .string(createdAtISO8601),
"tab_title": snapshot.tabTitle.map(JSONValue.string) ?? .null,
]
if includeReadState {
payload["is_read"] = .bool(snapshot.isRead)
Expand All @@ -354,6 +435,13 @@ extension ControlCommandCoordinator {
return payload
}

private nonisolated static func notificationTimestampFormatter() -> ISO8601DateFormatter {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}

// MARK: - Localized error messages

/// The localized notification messages from the app conformance, or the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ public import Foundation
/// ``ControlNotificationContext``.
///
/// Mirrors the app target's `TerminalNotification` (plus the two app-resolved
/// adornments the legacy `notificationPayload` builder added: the ISO-8601
/// `createdAt` rendering and the workspace's tab title) without the package
/// importing the app target. The coordinator turns each snapshot into a
/// notification payload object, byte-identically to the former
/// `[String: Any]` builder.
/// adornments the legacy `notificationPayload` builder added: the creation
/// time and workspace tab title) without the package importing the app target.
/// The coordinator renders the timestamp while building the wire payload on
/// the socket worker.
public struct ControlNotificationSnapshot: Sendable, Equatable {
/// The notification's stable identifier.
public let id: UUID
Expand All @@ -23,11 +22,9 @@ public struct ControlNotificationSnapshot: Sendable, Equatable {
public let subtitle: String
/// The notification body.
public let body: String
/// The creation timestamp pre-rendered exactly as the legacy
/// `notificationCreatedAtString` did (`ISO8601DateFormatter` with
/// `.withInternetDateTime`, GMT). Carried as a string so the package never
/// re-formats the date and the wire bytes stay identical.
public let createdAtISO8601: String
/// The creation timestamp. Kept as a Sendable value so full-list formatting
/// can run after the bounded main-actor snapshot hop.
public let createdAt: Date
/// Whether the notification has been marked read.
public let isRead: Bool
/// The workspace's tab title, if the app could resolve one (the legacy
Expand All @@ -43,7 +40,7 @@ public struct ControlNotificationSnapshot: Sendable, Equatable {
/// - title: The notification title.
/// - subtitle: The notification subtitle.
/// - body: The notification body.
/// - createdAtISO8601: The pre-rendered ISO-8601 creation timestamp.
/// - createdAt: The creation timestamp.
/// - isRead: Whether the notification is read.
/// - tabTitle: The owning workspace's tab title, if any.
public init(
Expand All @@ -53,7 +50,7 @@ public struct ControlNotificationSnapshot: Sendable, Equatable {
title: String,
subtitle: String,
body: String,
createdAtISO8601: String,
createdAt: Date,
isRead: Bool,
tabTitle: String?
) {
Expand All @@ -63,7 +60,7 @@ public struct ControlNotificationSnapshot: Sendable, Equatable {
self.title = title
self.subtitle = subtitle
self.body = body
self.createdAtISO8601 = createdAtISO8601
self.createdAt = createdAt
self.isRead = isRead
self.tabTitle = tabTitle
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,7 @@ public enum ControlCommandExecutionPolicy: Sendable, Equatable {
"surface.report_shell_state",
"surface.report_tty",
"surface.ports_kick",
// The notification-create family and workspace.set_auto_title run the
// same single-hop worker shape (parse/bridge/encode on the worker, one
// Notification creation and workspace.set_auto_title parse/bridge/encode on the worker, with one
// v2MainSync around the shared main-actor dispatch). The hop stays
// synchronous so the reply is written only after the hop body ran —
// matching the legacy main-lane ordering exactly. NOTE: that is NOT
Expand All @@ -237,6 +236,7 @@ public enum ControlCommandExecutionPolicy: Sendable, Equatable {
"notification.create_for_surface",
"notification.create_for_target",
"notification.create_for_caller",
"notification.list",
"workspace.set_auto_title",
// The v2 resolution reads (tranche D of issue #5757) — the implicit
// handle-normalization reads nearly every CLI invocation pays 1-3 of.
Expand Down Expand Up @@ -368,8 +368,10 @@ public enum ControlCommandExecutionPolicy: Sendable, Equatable {
/// `TerminalController`: parse/format on the worker; `notify_target_async`
/// and `clear_notifications` are pure mutation-bus enqueues (zero main
/// hops, hooks nohup them and discard the reply); the synchronous
/// notify/list verbs keep one `v2MainSync` hop because their replies
/// depend on tab/surface resolution or the delivered store state.
/// notify verbs keep one `v2MainSync` hop because their replies depend on
/// tab/surface resolution or delivered store state. `list_notifications`
/// snapshots on the main actor but formats on the worker and is not safe
/// to collapse inline for in-process main-thread callers.
/// Internal (not private) so the package tests can pin the exact set.
static let notificationV1Commands: Set<String> = [
"notify",
Expand Down Expand Up @@ -482,14 +484,14 @@ public enum ControlCommandExecutionPolicy: Sendable, Equatable {
"report_shell_state",
"report_tty",
"ports_kick",
// The v1 notification family (tranche B2): notify_target_async and
// clear_notifications are pure bus enqueues; the synchronous verbs
// are one inline-collapsing hop each.
// The v1 notification family (tranche B2): clear_notifications is a
// pure bus enqueue; synchronous notify verbs are one inline-collapsing
// hop each. list_notifications formats the full feed and
// notify_target_async can wait for bounded queue capacity, so both
// remain worker-only.
"notify",
"notify_surface",
"notify_target",
"notify_target_async",
"list_notifications",
"clear_notifications",
// The v1 resolution reads (tranche D): non-blocking single-hop
// snapshot reads whose hop collapses inline on a main-thread caller,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation
import Testing
@testable import CmuxControlSocket

@MainActor
private final class NotificationListControlCommandContext: ControlCommandContext {
private(set) var resolveOnMainCallCount = 0
var notifications: [ControlNotificationSnapshot] = []

nonisolated func controlResolveOnMain<T: Sendable>(
_ body: @MainActor (any ControlCommandContext) -> T
) -> T {
MainActor.assumeIsolated {
resolveOnMainCallCount += 1
return body(self)
}
}

func controlNotificationList() -> [ControlNotificationSnapshot] {
notifications
}
}

@MainActor
@Suite("ControlCommandCoordinator notification domain")
struct ControlCommandCoordinatorNotificationTests {
@Test func notificationListWorkerHandlerUsesOneMainHop() {
let context = NotificationListControlCommandContext()
let coordinator = ControlCommandCoordinator(context: context)
let request = ControlRequest(id: .int(1), method: "notification.list", params: [:])

guard case .ok(.object(let payload))? = coordinator.handleSocketWorkerV2(
request,
context: context
), case .array(let notifications) = payload["notifications"] else {
Issue.record("notification.list worker handler did not return its list payload")
return
}

#expect(context.resolveOnMainCallCount == 1)
#expect(notifications.isEmpty)
}

@Test func notificationListPreservesLargeSnapshotOrderAndWireShape() throws {
let context = NotificationListControlCommandContext()
let coordinator = ControlCommandCoordinator(context: context)
let workspaceID = UUID()
let surfaceID = UUID()
let snapshots = (0..<10_500).reversed().map { index in
ControlNotificationSnapshot(
id: UUID(),
workspaceID: workspaceID,
surfaceID: surfaceID,
title: "Notification \(index)",
subtitle: "Subtitle",
body: "Body",
createdAt: Date(timeIntervalSince1970: TimeInterval(index)),
isRead: index.isMultiple(of: 2),
tabTitle: "Workspace"
)
}
context.notifications = snapshots
let request = ControlRequest(id: .int(1), method: "notification.list", params: [:])

guard case .ok(.object(let payload))? = coordinator.handleSocketWorkerV2(
request,
context: context
), case .array(let notifications) = payload["notifications"],
case .object(let first) = notifications.first,
case .object(let last) = notifications.last else {
Issue.record("notification.list worker handler returned an unexpected payload")
return
}

#expect(context.resolveOnMainCallCount == 1)
#expect(notifications.count == snapshots.count)
#expect(first["id"] == .string(try #require(snapshots.first).id.uuidString))
#expect(first["created_at"] == .string("1970-01-01T02:54:59Z"))
#expect(first["workspace_ref"] == .string("workspace:1"))
#expect(first["surface_ref"] == .string("surface:1"))
#expect(last["id"] == .string(try #require(snapshots.last).id.uuidString))
#expect(last["created_at"] == .string("1970-01-01T00:00:00Z"))
}
}
Loading
Loading