@githolon/dsl 0.117.1 → 0.119.11
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.
- package/assets/operation-contract.wasm +0 -0
- package/compile_proof.mjs +16 -0
- package/dart/CHANGELOG.md +45 -0
- package/dart/lib/src/application_status.dart +58 -2
- package/dart/lib/src/peer_application_streams.dart +7 -1
- package/dart/lib/src/reception/dart_reception.dart +92 -12
- package/dart/lib/src/reception/reception_session.dart +10 -2
- package/dart/lib/src/reception/workspace_preparation_report.dart +50 -41
- package/dart/lib/src/replica_readiness.dart +200 -50
- package/dart/lib/src/runner/node-entry.mjs +2481 -1022
- package/dart/lib/src/sync_status_projection.dart +346 -4
- package/dart/lib/src/sync_status_reducer.dart +44 -0
- package/dart/lib/src/test_harness.dart +1 -0
- package/dart/lib/src/typed_watch_registry.dart +67 -7
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/copy_repair_status_test.dart +238 -0
- package/dart/test/reads_never_wait_test.dart +387 -0
- package/dart/test/replica_readiness_test.dart +223 -109
- package/dart/test/sync_status_reducer_test.dart +39 -0
- package/dart/test/typed_watch_registry_test.dart +69 -0
- package/dart/test/unshared_work_status_test.dart +269 -0
- package/dart/test/workspace_preparation_upkeep_test.dart +40 -6
- package/dart_types/CHANGELOG.md +18 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +320 -85
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +57 -1
- package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pb.dart +14 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +25 -1
- package/dart_types/lib/src/runtime_outcome.dart +17 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +5 -2
- package/src/codegen_ts.ts +7 -8
- package/src/compile_package_main.ts +11 -4
- package/src/reception_unit_artifact.ts +105 -0
- package/src/reception_unit_artifact_contract.ts +220 -0
- package/src/reception_unit_context.ts +47 -0
- package/src/reception_unit_source.ts +205 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// Work a sync round leaves owed is never "in sync", until a round shows progress (CAP-923).
|
|
2
|
+
//
|
|
3
|
+
// Jack's iPad (TestFlight 1338, 11 Oct 2026 14:32 UTC): the Lippold estate's round ended
|
|
4
|
+
// `deferred=1` with "Sharing is waiting for catalogue-0744 to become resident", the next round did
|
|
5
|
+
// nothing (pulled nothing, head and bookmark unchanged, deferred=0), and the status went back to
|
|
6
|
+
// in sync while the estate was 16 commits behind its server and the phone and iPad disagreed.
|
|
7
|
+
import 'dart:async';
|
|
8
|
+
|
|
9
|
+
import 'package:fixnum/fixnum.dart';
|
|
10
|
+
|
|
11
|
+
import 'package:nomos_client/src/nms_generated_runtime.dart';
|
|
12
|
+
import 'package:nomos_types/nomos_kernel_protocol.dart' as p;
|
|
13
|
+
import 'package:test/test.dart';
|
|
14
|
+
|
|
15
|
+
const _estate = 'estate-0fbc5c6a931205ae404d839df6ac9ff550b3ec82';
|
|
16
|
+
const _ipadHead = '236fa9084f68a4baa0d00db5f8ae9587ca0d5d61';
|
|
17
|
+
const _serverHead = 'cb90fef930fbf2310b62e8b5080ca4e70d7a616a';
|
|
18
|
+
const _localEdit = '38865f0f65601093e3b0df6b56af383337f52d40';
|
|
19
|
+
|
|
20
|
+
final class _Office
|
|
21
|
+
with NomosRuntimeV2KernelCalls
|
|
22
|
+
implements PeerOfficeConnection {
|
|
23
|
+
final events = StreamController<p.RuntimeEvent>.broadcast(sync: true);
|
|
24
|
+
int sequence = 0;
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
Stream<p.RuntimeEvent> get runtimeEvents => events.stream;
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
int nextRuntimeRequestId() => ++sequence;
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
Future<p.RuntimeResponse> invokeRuntime(p.RuntimeRequest request) async {
|
|
34
|
+
if (request.hasCloseRealm()) {
|
|
35
|
+
return p.RuntimeResponse(
|
|
36
|
+
protocolVersion: 2,
|
|
37
|
+
requestId: request.requestId,
|
|
38
|
+
realmClosed: p.RealmClosed(),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
fail('Unexpected command: ${request.whichCommand()}');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void round(p.SyncWorkspaceResponse result) => events.add(
|
|
45
|
+
p.RuntimeEvent(
|
|
46
|
+
session: _estate,
|
|
47
|
+
syncCompleted: p.SyncCompleted(result: result),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/// The 14:32:16 round: deferred on a dependency, the custodian's work not received.
|
|
52
|
+
void deferredRound({String head = _ipadHead, String tracked = _ipadHead}) =>
|
|
53
|
+
round(p.SyncWorkspaceResponse(
|
|
54
|
+
workspace: _estate,
|
|
55
|
+
previousHead: tracked,
|
|
56
|
+
currentHead: head,
|
|
57
|
+
deferred: Int64(1),
|
|
58
|
+
directOfferDeferred: true,
|
|
59
|
+
stillAhead: true,
|
|
60
|
+
reconnectRequired: true,
|
|
61
|
+
offerReceipt: p.CustodyOfferReceipt(
|
|
62
|
+
requestId: 'workspace-sync-9:dependency',
|
|
63
|
+
blocked: p.RuntimeBlocked(
|
|
64
|
+
kernelResidency: p.WorkspaceKernelResidencyRequired(
|
|
65
|
+
workspace: 'catalogue-0744dbfae089741266a9abfd9d86485bb6580a03',
|
|
66
|
+
detail: 'sync awaits catalogue-0744 state 51324b73',
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
));
|
|
71
|
+
|
|
72
|
+
/// The 14:32:34 round: nothing pulled, heads unchanged, nothing deferred.
|
|
73
|
+
void roundThatDidNothing(
|
|
74
|
+
{String head = _ipadHead, String tracked = _ipadHead}) =>
|
|
75
|
+
round(p.SyncWorkspaceResponse(
|
|
76
|
+
workspace: _estate,
|
|
77
|
+
previousHead: tracked,
|
|
78
|
+
currentHead: head,
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
void main() {
|
|
83
|
+
late _Office office;
|
|
84
|
+
late DartReception reception;
|
|
85
|
+
late NomosSession session;
|
|
86
|
+
|
|
87
|
+
setUp(() {
|
|
88
|
+
office = _Office();
|
|
89
|
+
reception = DartReception(office);
|
|
90
|
+
session = reception.session(_estate);
|
|
91
|
+
});
|
|
92
|
+
tearDown(() async {
|
|
93
|
+
await reception.dispose();
|
|
94
|
+
await office.events.close();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test(
|
|
98
|
+
'the iPad sequence: a deferred round, then a round that did nothing, is still not in sync',
|
|
99
|
+
() {
|
|
100
|
+
office.roundThatDidNothing();
|
|
101
|
+
expect(session.currentStatus.phase, SyncPhase.idle);
|
|
102
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
103
|
+
|
|
104
|
+
office.deferredRound();
|
|
105
|
+
expect(reception.currentStatus.unsharedWork.keys, [_estate]);
|
|
106
|
+
final owed = reception.currentStatus.unsharedWork[_estate]!;
|
|
107
|
+
expect(owed.outbound, isFalse,
|
|
108
|
+
reason: 'local main == bookmark: the owed work is the server\'s');
|
|
109
|
+
expect(owed.detail, contains('catalogue-0744'));
|
|
110
|
+
final since = owed.since;
|
|
111
|
+
|
|
112
|
+
office.roundThatDidNothing();
|
|
113
|
+
expect(session.currentStatus.phase, isNot(SyncPhase.idle));
|
|
114
|
+
expect(reception.currentStatus.phase, isNot(SyncPhase.idle));
|
|
115
|
+
expect(reception.currentStatus.unsharedWork.keys, [_estate]);
|
|
116
|
+
expect(reception.currentStatus.unsharedWork[_estate]!.since, since,
|
|
117
|
+
reason: 'the first sighting is kept for the attention timer');
|
|
118
|
+
expect(session.currentStatus.unsharedWork.keys, [_estate]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('a round that receives the server\'s work clears it', () {
|
|
122
|
+
office.deferredRound();
|
|
123
|
+
office.round(p.SyncWorkspaceResponse(
|
|
124
|
+
workspace: _estate,
|
|
125
|
+
previousHead: _serverHead,
|
|
126
|
+
currentHead: _serverHead,
|
|
127
|
+
pulled: true,
|
|
128
|
+
mainAdvanced: true,
|
|
129
|
+
));
|
|
130
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
131
|
+
expect(session.currentStatus.phase, SyncPhase.idle);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test(
|
|
135
|
+
'local changes not shared are outbound, and clear once the server admits them',
|
|
136
|
+
() {
|
|
137
|
+
office.deferredRound(head: _localEdit, tracked: _serverHead);
|
|
138
|
+
expect(reception.currentStatus.unsharedWork[_estate]!.outbound, isTrue);
|
|
139
|
+
office.roundThatDidNothing(head: _localEdit, tracked: _serverHead);
|
|
140
|
+
expect(reception.currentStatus.unsharedWork.keys, [_estate]);
|
|
141
|
+
|
|
142
|
+
office.round(p.SyncWorkspaceResponse(
|
|
143
|
+
workspace: _estate,
|
|
144
|
+
previousHead: _localEdit,
|
|
145
|
+
currentHead: _localEdit,
|
|
146
|
+
offerReceipt: p.CustodyOfferReceipt(
|
|
147
|
+
admission: p.CustodyOfferAdmissionReceipt(
|
|
148
|
+
main: _localEdit, sealedMain: _localEdit),
|
|
149
|
+
),
|
|
150
|
+
));
|
|
151
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
152
|
+
expect(session.currentStatus.phase, SyncPhase.idle);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('admitted by the server but not yet sealed is not owed work', () {
|
|
156
|
+
office.round(p.SyncWorkspaceResponse(
|
|
157
|
+
workspace: _estate,
|
|
158
|
+
previousHead: _serverHead,
|
|
159
|
+
currentHead: _localEdit,
|
|
160
|
+
stillAhead: true,
|
|
161
|
+
offerReceipt: p.CustodyOfferReceipt(
|
|
162
|
+
admission: p.CustodyOfferAdmissionReceipt(main: _localEdit),
|
|
163
|
+
),
|
|
164
|
+
));
|
|
165
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('a refetch that replaced the copy clears owed work', () {
|
|
169
|
+
office.deferredRound();
|
|
170
|
+
office.round(p.SyncWorkspaceResponse(
|
|
171
|
+
workspace: _estate,
|
|
172
|
+
previousHead: _ipadHead,
|
|
173
|
+
currentHead: _ipadHead,
|
|
174
|
+
custodyReplaced: true,
|
|
175
|
+
));
|
|
176
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test(
|
|
180
|
+
'a round answered "the server is opening this workspace" owes nothing: a level workspace '
|
|
181
|
+
'is not left "not caught up" after the server restarts', () {
|
|
182
|
+
// Prod web, 12 Oct 2026: a tab loaded a minute after the cloud restarted. Four level
|
|
183
|
+
// workspaces' frontier probes were answered "opening"; their next rounds levelled over the
|
|
184
|
+
// stream and moved no head, and the popover said "haven't caught up - retrying" for ever.
|
|
185
|
+
office.round(p.SyncWorkspaceResponse(
|
|
186
|
+
workspace: _estate,
|
|
187
|
+
previousHead: _ipadHead,
|
|
188
|
+
currentHead: _ipadHead,
|
|
189
|
+
deferred: Int64(1),
|
|
190
|
+
directOfferDeferred: true,
|
|
191
|
+
custodianWarmingRetryAfterMillis: 2000,
|
|
192
|
+
));
|
|
193
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
194
|
+
office.roundThatDidNothing();
|
|
195
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
196
|
+
expect(session.currentStatus.phase, SyncPhase.idle);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('a warming round still owes local changes above the bookmark', () {
|
|
200
|
+
office.round(p.SyncWorkspaceResponse(
|
|
201
|
+
workspace: _estate,
|
|
202
|
+
previousHead: _serverHead,
|
|
203
|
+
currentHead: _localEdit,
|
|
204
|
+
stillAhead: true,
|
|
205
|
+
custodianWarmingRetryAfterMillis: 2000,
|
|
206
|
+
));
|
|
207
|
+
expect(reception.currentStatus.unsharedWork[_estate]!.outbound, isTrue);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test(
|
|
211
|
+
'a level copy whose server has not sealed its main yet is not "not caught up"',
|
|
212
|
+
() {
|
|
213
|
+
// The round levelled over the stream at the head the copy holds; the server's seal of that
|
|
214
|
+
// main is still pending, so the round reports stillAhead with equal heads and no deferral.
|
|
215
|
+
office.round(p.SyncWorkspaceResponse(
|
|
216
|
+
workspace: _estate,
|
|
217
|
+
previousHead: _serverHead,
|
|
218
|
+
currentHead: _serverHead,
|
|
219
|
+
stillAhead: true,
|
|
220
|
+
));
|
|
221
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
222
|
+
// The same heads with a deferral are still owed work (the iPad sequence).
|
|
223
|
+
office.deferredRound(head: _serverHead, tracked: _serverHead);
|
|
224
|
+
expect(reception.currentStatus.unsharedWork.keys, [_estate]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test(
|
|
228
|
+
'a level round with nothing deferred clears an inbound record that no deferral set',
|
|
229
|
+
() {
|
|
230
|
+
// A round answered "another round is converging this workspace": no heads, nothing judged.
|
|
231
|
+
office.round(p.SyncWorkspaceResponse(
|
|
232
|
+
workspace: _estate,
|
|
233
|
+
stillAhead: true,
|
|
234
|
+
directOfferDeferred: true,
|
|
235
|
+
));
|
|
236
|
+
expect(reception.currentStatus.unsharedWork[_estate]!.outbound, isFalse);
|
|
237
|
+
office.roundThatDidNothing();
|
|
238
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
239
|
+
expect(session.currentStatus.phase, SyncPhase.idle);
|
|
240
|
+
|
|
241
|
+
// The same at one head throughout: the record's heads never move, the level round clears it.
|
|
242
|
+
office.round(p.SyncWorkspaceResponse(
|
|
243
|
+
workspace: _estate,
|
|
244
|
+
previousHead: _ipadHead,
|
|
245
|
+
currentHead: _ipadHead,
|
|
246
|
+
stillAhead: true,
|
|
247
|
+
directOfferDeferred: true,
|
|
248
|
+
));
|
|
249
|
+
expect(reception.currentStatus.unsharedWork.keys, [_estate]);
|
|
250
|
+
office.roundThatDidNothing();
|
|
251
|
+
expect(reception.currentStatus.unsharedWork, isEmpty);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('a level round does not clear unshared local changes', () {
|
|
255
|
+
office.round(p.SyncWorkspaceResponse(
|
|
256
|
+
workspace: _estate,
|
|
257
|
+
previousHead: _serverHead,
|
|
258
|
+
currentHead: _localEdit,
|
|
259
|
+
stillAhead: true,
|
|
260
|
+
));
|
|
261
|
+
expect(reception.currentStatus.unsharedWork[_estate]!.outbound, isTrue);
|
|
262
|
+
office.round(p.SyncWorkspaceResponse(
|
|
263
|
+
workspace: _estate,
|
|
264
|
+
previousHead: _serverHead,
|
|
265
|
+
currentHead: _localEdit,
|
|
266
|
+
));
|
|
267
|
+
expect(reception.currentStatus.unsharedWork.keys, [_estate]);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -37,12 +37,14 @@ void main() {
|
|
|
37
37
|
),
|
|
38
38
|
),
|
|
39
39
|
);
|
|
40
|
-
expect(
|
|
41
|
-
|
|
40
|
+
expect(
|
|
41
|
+
reads.active,
|
|
42
|
+
isTrue,
|
|
43
|
+
reason: 'a read index still being built is real preparation',
|
|
44
|
+
);
|
|
42
45
|
});
|
|
43
46
|
|
|
44
|
-
test('upkeep backing off after a failure is not a sync problem (CAP-693)',
|
|
45
|
-
() {
|
|
47
|
+
test('upkeep backing off after a failure is not a sync problem (CAP-693)', () {
|
|
46
48
|
// A shallow device's Git upkeep was refused by the kernel; the status turned orange though
|
|
47
49
|
// every read and write still worked.
|
|
48
50
|
final failingUpkeep = WorkspacePreparationReport.fromMaintenance(
|
|
@@ -63,7 +65,39 @@ void main() {
|
|
|
63
65
|
retryAfterMillis: 4000,
|
|
64
66
|
),
|
|
65
67
|
);
|
|
66
|
-
expect(
|
|
67
|
-
|
|
68
|
+
expect(
|
|
69
|
+
failingReads.retrying,
|
|
70
|
+
isTrue,
|
|
71
|
+
reason: 'preparation a read waits on still shows',
|
|
72
|
+
);
|
|
68
73
|
});
|
|
74
|
+
|
|
75
|
+
test(
|
|
76
|
+
'a pack tidy that fails after a sync is upkeep backing off, not a sync problem (CAP-952)',
|
|
77
|
+
() {
|
|
78
|
+
// Prod web, 13 Oct 2026: the kernel refused a repack after an incoming sync ("repack header
|
|
79
|
+
// …: object could not be found"). The sync icon stayed amber on "Still preparing" for the
|
|
80
|
+
// back-off while all 34 workspaces were up to date.
|
|
81
|
+
final failingAfterSync = WorkspacePreparationReport.fromMaintenance(
|
|
82
|
+
protocol.ReadMaintenanceProgress(
|
|
83
|
+
workspace: 'estate',
|
|
84
|
+
cause: protocol.ReadMaintenanceCause.READ_MAINTENANCE_CAUSE_SYNC,
|
|
85
|
+
retryAfterMillis: 1800000,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
expect(failingAfterSync.retrying, isFalse);
|
|
89
|
+
expect(failingAfterSync.upkeepRetrying, isTrue);
|
|
90
|
+
expect(failingAfterSync.active, isFalse);
|
|
91
|
+
|
|
92
|
+
// The read index a sync owes travels under its own read cause and still counts.
|
|
93
|
+
final indexAfterSync = WorkspacePreparationReport.fromMaintenance(
|
|
94
|
+
protocol.ReadMaintenanceProgress(
|
|
95
|
+
workspace: 'estate',
|
|
96
|
+
cause: protocol.ReadMaintenanceCause.READ_MAINTENANCE_CAUSE_RESIDENCY,
|
|
97
|
+
retryAfterMillis: 1000,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
expect(indexAfterSync.retrying, isTrue);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
69
103
|
}
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 0.15.2
|
|
2
|
+
|
|
3
|
+
Add the shared kernel protocol fields for owned pack import continuations, cancellation and deferred admission.
|
|
4
|
+
|
|
5
|
+
- Kernel protocol: `QueryRequest.serve_last_ready`, `QueryResponse.read_index_behind` (`ReadIndexBehind`) and `RUNTIME_UNAVAILABLE_REASON_READ_INDEX_REFRESHING_SUCCESSOR_LAW` (CAP-891).
|
|
6
|
+
|
|
7
|
+
- `ReadIndexPending.lastReadyAbsent` and `LocalReadPending.heldReason` carry why a read is held for read maintenance (CAP-900).
|
|
8
|
+
|
|
9
|
+
- `NomosUnavailableReason.readIndexRefreshing` types `RUNTIME_UNAVAILABLE_REASON_READ_INDEX_REFRESHING_SUCCESSOR_LAW`, which mapped to `unknown`. The generated Dart protocol bindings now carry the CAP-891 additions (`serve_last_ready`, `read_index_behind`, reason 8); they had not been regenerated (CAP-900).
|
|
10
|
+
|
|
11
|
+
- `NomosUnavailableReason.firstDownload` types the new `RUNTIME_UNAVAILABLE_REASON_FIRST_DOWNLOAD` (CAP-913): this workspace has never been on this device and its first download is under way. A write to it is refused at once with this reason. It is distinct from `localReplicaNotReady`.
|
|
12
|
+
|
|
13
|
+
- `PeerSocketObserved.rungWorkspaces` (CAP-931): the workspaces whose upstream rang, while the socket receipt was handled, for a main this copy does not hold. Each one's follow round is due now.
|
|
14
|
+
|
|
15
|
+
## 0.15.1
|
|
16
|
+
|
|
17
|
+
- `LiveFrontier.heldTrees`, `LiveCheckpointRequest.heldTrees`, `LiveCheckpointOffer.omittedTrees`/`sharedTrees` and `PeerStreamReceipt.omittedTrees` let a copy receive a checkpoint without the law closure it already holds (CAP-901).
|
|
18
|
+
|
|
1
19
|
## 0.15.0
|
|
2
20
|
|
|
3
21
|
- `FollowObservationPhase.FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED`: this peer's kernel deterministically refused a replica follow's round, so the follow's sync is blocked and is not retrying.
|