@githolon/dsl 0.117.0 → 0.119.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/compile_proof.mjs +16 -0
  3. package/dart/CHANGELOG.md +45 -0
  4. package/dart/lib/src/application_status.dart +58 -2
  5. package/dart/lib/src/peer_application_streams.dart +7 -1
  6. package/dart/lib/src/reception/dart_reception.dart +92 -12
  7. package/dart/lib/src/reception/reception_session.dart +10 -2
  8. package/dart/lib/src/replica_readiness.dart +200 -50
  9. package/dart/lib/src/runner/node-entry.mjs +2480 -1021
  10. package/dart/lib/src/sync_status_projection.dart +346 -4
  11. package/dart/lib/src/sync_status_reducer.dart +44 -0
  12. package/dart/lib/src/test_harness.dart +1 -0
  13. package/dart/lib/src/typed_watch_registry.dart +67 -7
  14. package/dart/pubspec.lock +1 -1
  15. package/dart/pubspec.yaml +2 -2
  16. package/dart/test/copy_repair_status_test.dart +238 -0
  17. package/dart/test/reads_never_wait_test.dart +387 -0
  18. package/dart/test/replica_readiness_test.dart +223 -109
  19. package/dart/test/sync_status_reducer_test.dart +39 -0
  20. package/dart/test/typed_watch_registry_test.dart +69 -0
  21. package/dart/test/unshared_work_status_test.dart +269 -0
  22. package/dart_types/CHANGELOG.md +18 -0
  23. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +320 -85
  24. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +57 -1
  25. package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pb.dart +14 -0
  26. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +25 -1
  27. package/dart_types/lib/src/runtime_outcome.dart +17 -0
  28. package/dart_types/pubspec.yaml +1 -1
  29. package/package.json +6 -2
  30. package/src/codegen_ts.ts +7 -8
  31. package/src/compile_package_main.ts +11 -4
  32. package/src/reception_template.ts +4 -3
  33. package/src/reception_unit_artifact.ts +105 -0
  34. package/src/reception_unit_artifact_contract.ts +220 -0
  35. package/src/reception_unit_context.ts +47 -0
  36. package/src/reception_unit_source.ts +205 -0
Binary file
package/compile_proof.mjs CHANGED
@@ -94,6 +94,21 @@ function dependencyPackageFiles(dslDir, cwd) {
94
94
  return files;
95
95
  }
96
96
 
97
+ function receptionRuntimeFiles(dslDir) {
98
+ // Reception's esbuild graph is loaded outside the compiler's TS import hooks.
99
+ // Match its compiler-owned resolution, rather than a caller's runtime package.
100
+ // Even tree-shaken protocol text affects esbuild's identifier frequencies.
101
+ try {
102
+ const request = createRequire(pathToFileURL(path.join(dslDir, "src", "reception_template.ts")));
103
+ const manifest = request.resolve("@githolon/runtime/package.json");
104
+ return [manifest, ...walk(path.join(path.dirname(manifest), "src"),
105
+ (file) => INPUT_EXTENSIONS.has(path.extname(file)))];
106
+ } catch {
107
+ // A removed dependency changes the inventory; the compiler reports its absence on a miss.
108
+ return [];
109
+ }
110
+ }
111
+
97
112
  /**
98
113
  * The authored module a running compile imported, or undefined for compiler and dependency code.
99
114
  * A project may name modules outside its own directory (`../../domains/x.ts`); walking the project
@@ -141,6 +156,7 @@ export function compileInputs({ cwd, dslDir, configPath, argumentIdentity, impor
141
156
  path.join(dslDir, "operation_contract_check.mjs"),
142
157
  path.join(dslDir, "assets", "operation-contract.wasm"),
143
158
  ...dependencyPackageFiles(dslDir, cwd),
159
+ ...receptionRuntimeFiles(dslDir),
144
160
  ]);
145
161
  return {
146
162
  authoring,
package/dart/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## 0.70.10
2
+
3
+ Deliver the shared runtime's owned, bounded pack import continuations and cancellation with the matching kernel protocol, preventing concurrent workspace acquisition from dropping an active import.
4
+
5
+ - Release with the runtime whose held reads repay their own workspace's index first (CAP-890).
6
+
7
+ - Release with the runtime whose local reads answer from the last-ready read index and type a successor-law refresh as a retryable unavailable (CAP-891).
8
+
9
+ - Release with the runtime whose cold read-index rebuild keeps the last-ready frontier serving reads until the rebuilt index switches in (CAP-900).
10
+
11
+ - A one-shot read whose workspace's read index is being rebuilt under the law its head just adopted waits for the rebuilt index (re-asking near the runtime's retryAfter) instead of throwing `NomosUnavailable(unknown, ...)`. On Jack's phone (1.66.17) `upsertUserEstateAccess` failed this way right after the Home pulled the 2026.10.2 law (CAP-900).
12
+
13
+ - Release with the runtime whose checkpoint transfers are pipelined and run two at a time, with the checkpoint wait limit for remounts (CAP-901).
14
+
15
+ - Reads never wait on the network (CAP-908). A one-shot application data read (rows, a page, by-id, by-type or spatial rows, a count, a sum, and their historic forms) of a workspace this device does not hold yet, is still opening, or is re-indexing under a law this app cannot decode, now answers at once: an empty result, with the session's `SyncStatus.localCopy` and `ApplicationStatus.localCopy` set to `LocalCopyReadiness.arriving` or `.updating` (`ApplicationStatus.localCopyArriving`, `localCopyUpdating`, `localCopyPending`). A read that answers sets it back to `ready`. A framework read with no empty form (child workspaces) reports the typed `NomosUnavailable` at once. `nmsUntilReplicaReady`, which re-asked such reads for up to 10 minutes, is gone. On Jack's iPhone (TestFlight 1334, 11 Oct 2026) a never-fetched data profile's read was held 11 s behind a full-panel spinner.
16
+ - A watch on such a workspace emits the empty result and the status at once, then registers in the background (woken by the workspace's sync) and emits the rows; it stops re-asking as soon as the peer delivers its first result.
17
+
18
+ - `LocalCopyReadiness.firstDownload` and `ApplicationStatus.localCopyFirstDownload` (CAP-913). A read of a workspace that has never been on this device answers empty at once, and says so: `firstDownload`, not merely `arriving`. Once the copy lands, the status is `arriving` while its indexes are prepared, then `ready` when a read answers from it. Admission's raw frontier index is built before the read model, so `ready` also means a write will not wait on an index. `localCopyArriving` stays true for both `firstDownload` and `arriving`. A write to such a workspace throws `NomosUnavailable(firstDownload)` at once, asked once.
19
+
20
+ - `LocalCopyReadiness.repairing` (CAP-920): a workspace's copy on this device is damaged and being repaired (its local Git history is incomplete, it cannot be read, or it is being refetched). Derived from a read answering `localReplicaNotReady` with such a reason and from a sync round failing because a commit of the copy is unreadable; held per workspace until a read answers from the copy again or a refetch replaces it. `SyncStatus.repairingCopies` / `ApplicationStatus.repairingCopies` list every such workspace with when it was first seen, `ApplicationStatus.localCopyRepairing` says it of a session, and `ApplicationStatus.workspace` names a session's workspace. A completed sync round no longer resets a session's `localCopy`. Status only.
21
+
22
+ ## 0.70.9
23
+
24
+ Carry the runtime and held-law checkpoint protocol alongside sibling law supply in the hosted-client composition.
25
+
26
+ ## 0.70.8
27
+
28
+ - Refresh the bundled Peer Office runtime identity alongside the hosted SDK closure-hint release. Authority, signatures and runtime behavior are unchanged.
29
+
30
+ ## 0.70.7
31
+
32
+ - Release the bundled peer runner alongside the fixed npm runtime release for held-unit source custody reads.
33
+
34
+ ## 0.70.6
35
+
36
+ - Refresh the bundled peer runtime identity for the framework release that adds per-unit hosted Reception artifacts.
37
+
38
+ ## 0.70.5
39
+
40
+ - Release the generated runner restaged with the CAP-888 compiler and CLI package versions.
41
+
42
+ ## 0.70.4
43
+
44
+ - Refresh the bundled Node runner alongside the fixed npm-group release for deterministic compiler Reception encoding. Give the changed runner a new immutable client package version.
45
+
1
46
  ## 0.70.3
2
47
 
3
48
  Refresh the packaged Peer Office runner from the shared client source used by the hosted headless application client. The existing text-decoder BOM behavior is explicit; the kernel and Peer Office protocols are unchanged.
@@ -1,4 +1,5 @@
1
- import 'sync_status_projection.dart' show WorkspaceFollowStatus;
1
+ import 'sync_status_projection.dart'
2
+ show LocalCopyReadiness, UnsharedSyncWork, WorkspaceFollowStatus;
2
3
 
3
4
  /// Read-only presentation of one Peer Office workspace handle.
4
5
  enum ApplicationStatusPhase {
@@ -33,10 +34,64 @@ final class ApplicationStatus {
33
34
  this.preparingWorkspaces = 0,
34
35
  this.retryingWorkspaces = 0,
35
36
  this.preparationActivity,
37
+ this.localCopy = LocalCopyReadiness.ready,
38
+ this.repairingCopies = const {},
39
+ this.unsharedWork = const {},
40
+ this.workspace,
36
41
  });
37
42
 
43
+ /// Every workspace with sync work owed between this device and its server: changes made here
44
+ /// that have not reached it, or the server's changes not yet taken in here. A round that did
45
+ /// nothing does not clear it. Status only: a presentation never counts these as in sync, says
46
+ /// so calmly ("N changes on this device haven't reached the server yet — retrying"), and turns
47
+ /// to attention once one has been owed for a few minutes ([UnsharedSyncWork.since]).
48
+ final Map<String, UnsharedSyncWork> unsharedWork;
49
+
50
+ /// This session's own workspace has work owed to or from its server.
51
+ bool get hasUnsharedWork =>
52
+ workspace != null ? unsharedWork.containsKey(workspace) : unsharedWork.isNotEmpty;
53
+
54
+ /// The workspace a session's status describes; null on the application's own status.
55
+ final String? workspace;
56
+
38
57
  final ApplicationStatusPhase phase;
39
58
 
59
+ /// Whether this workspace's reads answer from a copy on this device. Reads never wait for a copy
60
+ /// to arrive: while it is [LocalCopyReadiness.arriving] or [LocalCopyReadiness.updating] they
61
+ /// answer empty at once and answer again (watches emit again) when it is here.
62
+ final LocalCopyReadiness localCopy;
63
+
64
+ /// This workspace is not readable on this device yet (its first download, or a copy here whose
65
+ /// indexes are still being prepared); its download runs in the background as the application's
66
+ /// own work. Its reads answered empty, not "none".
67
+ bool get localCopyArriving =>
68
+ localCopy == LocalCopyReadiness.arriving ||
69
+ localCopy == LocalCopyReadiness.firstDownload;
70
+
71
+ /// This workspace has never been on this device and its first download is under way: writes to
72
+ /// it are refused at once (NomosUnavailableReason.firstDownload). One-time per workspace per
73
+ /// device. Once the copy lands this is false, [localCopyArriving] stays true while its indexes are
74
+ /// prepared, and editing is ready when [localCopy] is [LocalCopyReadiness.ready].
75
+ bool get localCopyFirstDownload =>
76
+ localCopy == LocalCopyReadiness.firstDownload;
77
+
78
+ /// This workspace's copy is being re-indexed under a newer law; its reads answered empty for now.
79
+ bool get localCopyUpdating => localCopy == LocalCopyReadiness.updating;
80
+
81
+ /// Reads of this workspace answered empty because its copy is not readable here yet.
82
+ bool get localCopyPending => localCopy != LocalCopyReadiness.ready;
83
+
84
+ /// This workspace's copy on this device is damaged and being repaired (its local Git history is
85
+ /// incomplete, it cannot be read, or it is being refetched). You can look around what it shows;
86
+ /// writes to it cannot be admitted until it is repaired. Never "in sync", whatever its sync
87
+ /// rounds report.
88
+ bool get localCopyRepairing => localCopy == LocalCopyReadiness.repairing;
89
+
90
+ /// Every workspace whose copy on this device is damaged and being repaired, with when this peer
91
+ /// first saw it so (the same on the application's status and on each session's). Status only: a
92
+ /// presentation does not count these as in sync and holds editing back for them.
93
+ final Map<String, DateTime> repairingCopies;
94
+
40
95
  /// Null counts have not been reported by the runtime; they are not zero.
41
96
  final int? localChanges;
42
97
  final int? availableChanges;
@@ -76,5 +131,6 @@ final class ApplicationStatus {
76
131
  bool get isCurrent =>
77
132
  phase == ApplicationStatusPhase.current &&
78
133
  localChanges == 0 &&
79
- availableChanges == 0;
134
+ availableChanges == 0 &&
135
+ !hasUnsharedWork;
80
136
  }
@@ -15,6 +15,7 @@ abstract interface class NmsPeerApplicationStreams {
15
15
  String workspace,
16
16
  );
17
17
  Stream<void> get childWorkspaces;
18
+
18
19
  /// Any accepted local or remote change which may invalidate a generated
19
20
  /// application projection. Interpretation remains in generated law-aware code.
20
21
  Stream<void> get workspaceChanges;
@@ -31,17 +32,22 @@ final class NmsPeerApplicationStreamView implements NmsPeerApplicationStreams {
31
32
  const NmsPeerApplicationStreamView({
32
33
  required this.session,
33
34
  required this.statusProjection,
35
+ this.sessionStatusProjection,
34
36
  this.workspaceOpenProgressSource,
35
37
  });
36
38
 
37
39
  final NomosSession session;
38
40
  final ApplicationStatus Function(SyncStatus status) statusProjection;
41
+
42
+ /// The projection of this session's own status (it names the session's workspace); defaults to
43
+ /// [statusProjection].
44
+ final ApplicationStatus Function(SyncStatus status)? sessionStatusProjection;
39
45
  final Stream<ApplicationWorkspaceOpenProgress> Function(String workspace)?
40
46
  workspaceOpenProgressSource;
41
47
 
42
48
  @override
43
49
  Stream<ApplicationStatus> get status =>
44
- session.syncStatus.map(statusProjection);
50
+ session.syncStatus.map(sessionStatusProjection ?? statusProjection);
45
51
 
46
52
  @override
47
53
  Stream<ApplicationStatus> get applicationStatus =>
@@ -68,7 +68,12 @@ import '../application_framework_calls.dart'
68
68
  import '../kernel_primitives.dart' show NmsKernelPrimitives;
69
69
  import '../nomos_trace.dart' show NomosTrace;
70
70
  import '../observed_session_directory.dart' show NmsObservedSessionDirectory;
71
- import '../replica_readiness.dart' show nmsUntilReplicaReady;
71
+ import '../replica_readiness.dart'
72
+ show
73
+ NmsLocalCopyReport,
74
+ nmsPendingQueryResponse,
75
+ nmsPendingWatchChanged,
76
+ nmsReadNow;
72
77
  import '../rows_watch.dart' show NmsRowsMerge, nmsMergeRows;
73
78
  import '../operation_receipts.dart'
74
79
  show NmsOperationReceiptProjection, NomosOperationReceipt;
@@ -85,11 +90,17 @@ import 'progress_desk.dart'
85
90
  import '../signing_identity_kernel.dart' show NmsSigningIdentityKernel;
86
91
  import '../sync_status_reducer.dart';
87
92
  import '../sync_status_projection.dart'
88
- show SyncPhase, SyncStatus, TransferProgress, WorkspaceFollowStatus;
93
+ show
94
+ LocalCopyReadiness,
95
+ SyncPhase,
96
+ SyncStatus,
97
+ TransferProgress,
98
+ WorkspaceFollowStatus;
89
99
  import '../typed_watch_registry.dart' show NmsTypedWatchHandle;
90
100
 
91
101
  export '../sync_status_projection.dart'
92
102
  show
103
+ LocalCopyReadiness,
93
104
  SyncPhase,
94
105
  SyncStatus,
95
106
  TransferProgress,
@@ -964,6 +975,28 @@ class DartReception {
964
975
  session,
965
976
  (current) => reduceSyncCompletion(current, completed, observedAt),
966
977
  );
978
+ // A round may "complete" over a damaged copy (nothing pulled, head unchanged) while its
979
+ // kernel cannot read a commit of it; that is reported as a separate failure. Either says
980
+ // the copy is being repaired, and a refetch that replaced it says it is repaired.
981
+ final syncStatuses = _eventPlane._events.syncStatuses;
982
+ final damaged = nmsDamagedCopyOf(completed, session: session);
983
+ if (damaged != null) {
984
+ syncStatuses.markRepairing(damaged, at: observedAt);
985
+ } else if (completed.hasResult() &&
986
+ completed.result.custodyReplaced &&
987
+ completed.result.workspace.isNotEmpty) {
988
+ syncStatuses.markRepaired(completed.result.workspace);
989
+ }
990
+ // Work a round leaves owed stays owed until a round shows progress (CAP-923).
991
+ if (!completed.hasFailure() && completed.hasResult()) {
992
+ final result = completed.result;
993
+ final workspace =
994
+ result.workspace.isNotEmpty ? result.workspace : session;
995
+ if (workspace != null) {
996
+ syncStatuses.observeRound(workspace, result,
997
+ at: observedAt, detail: nmsOwedWorkDetail(result));
998
+ }
999
+ }
967
1000
  return;
968
1001
  }
969
1002
  if (event.hasLawChanged()) {
@@ -2136,12 +2169,13 @@ class DartReception {
2136
2169
  /// ledger's verified birth witnesses and lifecycle facts; tenant code only receives safe child links.
2137
2170
  ///
2138
2171
  /// On a cold mount the office answers "not ready yet" while it prepares the parent's birth
2139
- /// records (CAP-824); this waits that out like every other one-shot read, so a caller sees the
2140
- /// children or a real error, never an empty list that fills in by itself a moment later.
2172
+ /// records (CAP-824). That answer is reported at once as the typed [NomosUnavailable], never
2173
+ /// waited out and never an empty list: the children are not "none", they are not readable here
2174
+ /// yet, and the session's [SyncStatus.localCopy] says so.
2141
2175
  Future<List<nomos_protocol.ChildWorkspace>> nomosFrameworkChildWorkspaces(
2142
2176
  {String? namespace}) async {
2143
2177
  final session = _runtimeSession();
2144
- final response = await nmsUntilReplicaReady(
2178
+ final response = await nmsReadNow<nomos_protocol.RuntimeResponse>(
2145
2179
  () => _invokeRuntimeV2(
2146
2180
  nomos_protocol.RuntimeRequest(
2147
2181
  listChildWorkspaces: nomos_protocol.ListChildWorkspacesRequest(
@@ -2150,7 +2184,8 @@ class DartReception {
2150
2184
  ),
2151
2185
  ),
2152
2186
  ),
2153
- synced: _eventPlane._events.workspacesSynced,
2187
+ pending: (_) => null,
2188
+ report: _localCopyReport(session),
2154
2189
  );
2155
2190
  if (!response.hasChildWorkspaces()) {
2156
2191
  throw StateError(
@@ -2598,6 +2633,36 @@ class DartReception {
2598
2633
  );
2599
2634
  }
2600
2635
 
2636
+ /// The empty answer of an application data read whose copy is not readable here yet (see
2637
+ /// replica_readiness.dart), as the kernel call response the read decodes. Null when the query
2638
+ /// has no empty form: the typed "not ready" is then reported as it is.
2639
+ nomos_protocol.CallResponse? _pendingKernelCall(
2640
+ nomos_protocol.QueryRequest query,
2641
+ ) {
2642
+ final empty = nmsPendingQueryResponse(query);
2643
+ return empty == null ? null : nomos_protocol.CallResponse(query: empty);
2644
+ }
2645
+
2646
+ /// Records what a read found of [session]'s local copy on its sync status, so the application's
2647
+ /// status says "arriving" or "updating" while its reads answer empty, and "ready" once a read
2648
+ /// answers from the copy. Only a change is pushed.
2649
+ NmsLocalCopyReport _localCopyReport(String session) => (readiness) {
2650
+ final projection = _eventPlane._events.syncStatuses;
2651
+ // A session is pinned to its workspace by name. A damaged copy is recorded per workspace
2652
+ // (it is held "repairing" until a read answers from it again); see markRepairing.
2653
+ if (readiness == LocalCopyReadiness.repairing) {
2654
+ projection.markRepairing(session);
2655
+ return;
2656
+ }
2657
+ if (readiness == LocalCopyReadiness.ready)
2658
+ projection.markRepaired(session);
2659
+ if (projection.statusFor(session).localCopy == readiness) return;
2660
+ _updateScopedStatus(
2661
+ session,
2662
+ (current) => current.copyWith(localCopy: readiness),
2663
+ );
2664
+ };
2665
+
2601
2666
  Future<nomos_protocol.QueryResponse> _nmsKernelQuery({
2602
2667
  required String? session,
2603
2668
  required nomos_protocol.QueryAuthority authority,
@@ -2624,15 +2689,15 @@ class DartReception {
2624
2689
  // Correlation therefore belongs to the plane just like watches and legacy
2625
2690
  // reception calls. Keeping this counter on a child view lets two views mint
2626
2691
  // overlapping logical request identities despite sharing one executor.
2627
- // A copy still being fetched or opened answers "not ready yet": ask again
2628
- // rather than hand every caller a state that passes by itself.
2692
+ // A copy not readable here yet answers at once: empty for an application data
2693
+ // read, with the session's local copy reported arriving or updating, never a wait.
2629
2694
  final traceCall = NomosTrace.enabled ? NomosTrace.nextCall() : 0;
2630
2695
  final traceBegin = NomosTrace.enabled ? NomosTrace.now() : 0;
2631
2696
  if (NomosTrace.enabled) {
2632
2697
  NomosTrace.emit('dart', 'call.begin',
2633
2698
  'c=$traceCall op=${NomosTrace.describeCall(nomos_protocol.CallRequest(query: query))}');
2634
2699
  }
2635
- final response = await nmsUntilReplicaReady(
2700
+ final response = await nmsReadNow<nomos_protocol.CallResponse>(
2636
2701
  () {
2637
2702
  final requestNumber = ++plane._sequences.request;
2638
2703
  if (NomosTrace.enabled) {
@@ -2656,7 +2721,8 @@ class DartReception {
2656
2721
  ),
2657
2722
  );
2658
2723
  },
2659
- synced: _eventPlane._events.workspacesSynced,
2724
+ pending: (_) => _pendingKernelCall(query),
2725
+ report: _localCopyReport(runtimeSession),
2660
2726
  );
2661
2727
  if (NomosTrace.enabled) {
2662
2728
  NomosTrace.emit(
@@ -2888,7 +2954,7 @@ class DartReception {
2888
2954
  ..authority = authority
2889
2955
  ..spans = true
2890
2956
  ..resultFormat = resultFormat;
2891
- final response = await nmsUntilReplicaReady(
2957
+ final response = await nmsReadNow<nomos_protocol.RuntimeResponse>(
2892
2958
  () => _invokeRuntimeV2(
2893
2959
  nomos_protocol.RuntimeRequest(
2894
2960
  historicQuery: nomos_protocol.HistoricQueryRequest(
@@ -2898,7 +2964,16 @@ class DartReception {
2898
2964
  ),
2899
2965
  ),
2900
2966
  ),
2901
- synced: _eventPlane._events.workspacesSynced,
2967
+ pending: (_) {
2968
+ final empty = _pendingKernelCall(query);
2969
+ return empty == null
2970
+ ? null
2971
+ : nomos_protocol.RuntimeResponse(
2972
+ protocolVersion: 2,
2973
+ kernelCall: empty,
2974
+ );
2975
+ },
2976
+ report: _localCopyReport(runtimeSession),
2902
2977
  );
2903
2978
  if (response.hasFailure()) {
2904
2979
  throw StateError('${response.failure.code}: ${response.failure.message}');
@@ -3221,6 +3296,11 @@ class DartReception {
3221
3296
  : '',
3222
3297
  decode: decode,
3223
3298
  stop: _unwatch,
3299
+ // A copy not readable here yet: the watch shows this empty answer at once and the
3300
+ // session's local copy as arriving or updating, then its registration's first result.
3301
+ pending: (subscriptionId) =>
3302
+ nmsPendingWatchChanged(request, subscriptionId),
3303
+ report: _localCopyReport(runtimeSession),
3224
3304
  start: (subscriptionId) async {
3225
3305
  final response = await _invokeRuntimeV2(
3226
3306
  nomos_protocol.RuntimeRequest(
@@ -127,6 +127,8 @@ final class NmsReceptionSessionView implements NmsReceptionSession {
127
127
  streams = NmsPeerApplicationStreamView(
128
128
  session: desk,
129
129
  statusProjection: _applicationStatus,
130
+ sessionStatusProjection: (status) =>
131
+ _applicationStatus(status, workspace: desk.workspace),
130
132
  workspaceOpenProgressSource: watchOpenProgressCapability,
131
133
  );
132
134
 
@@ -143,7 +145,8 @@ final class NmsReceptionSessionView implements NmsReceptionSession {
143
145
  final Future<void> Function()? closeCapability;
144
146
 
145
147
  @override
146
- ApplicationStatus get currentStatus => _applicationStatus(desk.currentStatus);
148
+ ApplicationStatus get currentStatus =>
149
+ _applicationStatus(desk.currentStatus, workspace: desk.workspace);
147
150
 
148
151
  @override
149
152
  Future<void> whenCurrent({required Duration timeout}) =>
@@ -237,7 +240,9 @@ final class NmsReceptionSessionView implements NmsReceptionSession {
237
240
  Future<void> close() => closeCapability?.call() ?? Future<void>.value();
238
241
  }
239
242
 
240
- ApplicationStatus _applicationStatus(SyncStatus status) => ApplicationStatus(
243
+ ApplicationStatus _applicationStatus(SyncStatus status, {String? workspace}) =>
244
+ ApplicationStatus(
245
+ workspace: workspace,
241
246
  phase: switch (status.presentationPhase) {
242
247
  SyncPhase.checking => ApplicationStatusPhase.checking,
243
248
  SyncPhase.idle => ApplicationStatusPhase.current,
@@ -267,6 +272,9 @@ ApplicationStatus _applicationStatus(SyncStatus status) => ApplicationStatus(
267
272
  preparingWorkspaces: status.preparingWorkspaces,
268
273
  retryingWorkspaces: status.retryingWorkspaces,
269
274
  preparationActivity: status.preparationActivity,
275
+ localCopy: status.localCopy,
276
+ repairingCopies: status.repairingCopies,
277
+ unsharedWork: status.unsharedWork,
270
278
  );
271
279
 
272
280
  Future<void> _whenCurrent(DartReception application, Duration timeout) async {