@cotal-ai/manager 0.42.0 → 0.44.0
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/dist/commands.js +5 -2
- package/dist/commands.js.map +1 -1
- package/dist/deregister-instance.d.ts +5 -1
- package/dist/deregister-instance.d.ts.map +1 -1
- package/dist/deregister-instance.js +15 -3
- package/dist/deregister-instance.js.map +1 -1
- package/dist/launch.d.ts +1 -0
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +2 -0
- package/dist/launch.js.map +1 -1
- package/dist/manager.d.ts +15 -1
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +78 -20
- package/dist/manager.js.map +1 -1
- package/dist/reconcile-gate.d.ts +10 -1
- package/dist/reconcile-gate.d.ts.map +1 -1
- package/dist/reconcile-gate.js +43 -13
- package/dist/reconcile-gate.js.map +1 -1
- package/package.json +8 -8
package/dist/manager.js
CHANGED
|
@@ -372,6 +372,16 @@ export class Manager {
|
|
|
372
372
|
* hear `not-found`, which a seat reads as "drop it": failure reported for work that committed.
|
|
373
373
|
* The settled answer lives here for a bounded window instead, and the sweep drops it after,
|
|
374
374
|
* which is also what keeps this map from being one entry per turn for the process lifetime.
|
|
375
|
+
*
|
|
376
|
+
* `ref` is the goal coordinates the durable terminal is published under. A late yield that
|
|
377
|
+
* arrives after pendingTurns is gone and before `settled` is remembered (or after a concurrent
|
|
378
|
+
* sweep dropped the settled field) still has to name that terminal, never `not-found`. The
|
|
379
|
+
* index entry is not that answer: it is cleared at commit, and the yield path never consults it.
|
|
380
|
+
*
|
|
381
|
+
* `leftoverSince` is when the pending latch dropped. An unsettled leftover ages off that stamp,
|
|
382
|
+
* never off `acceptance.deadlineAt`: a turn that expired after a long outage would otherwise be
|
|
383
|
+
* pruned on the first sweep tick, taking the GoalRef with it, and a late yield in the retry
|
|
384
|
+
* window would hear `not-found` again.
|
|
375
385
|
*/
|
|
376
386
|
turnAcceptances = new Map();
|
|
377
387
|
/** Every relayed turn awaiting its seat's yield, by goal id. The seat's `turn-pending` pull
|
|
@@ -1353,13 +1363,25 @@ export class Manager {
|
|
|
1353
1363
|
*/
|
|
1354
1364
|
async deregisterServiceOnStop() {
|
|
1355
1365
|
const iid = this.managerInstanceId;
|
|
1356
|
-
const dereg = ({ recordsKv }) => deregisterServiceInstance(recordsKv, {
|
|
1366
|
+
const dereg = ({ recordsKv, authKv }) => deregisterServiceInstance(recordsKv, {
|
|
1367
|
+
endpoint: MANAGER_ENDPOINT,
|
|
1368
|
+
instanceId: iid,
|
|
1369
|
+
observeGeneration: async () => {
|
|
1370
|
+
const key = epgateKey(MANAGER_ENDPOINT, iid);
|
|
1371
|
+
const entry = await authKv.get(key);
|
|
1372
|
+
if (!entry || entry.operation !== "PUT")
|
|
1373
|
+
throw new Error(`no issuance gate at ${key}`);
|
|
1374
|
+
return parseEndpointGate(entry.value, key).generation;
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1357
1377
|
try {
|
|
1358
1378
|
const outcome = await (this.auth ? this.withEndpointServeExecutor(dereg) : this.withOpenServeConnection(dereg));
|
|
1359
1379
|
if (outcome.removed)
|
|
1360
1380
|
console.error(`✓ deregistered manager instance ${iid} from the ${MANAGER_ENDPOINT} service registry (spec revision ${outcome.specRevision})`);
|
|
1361
1381
|
else if (outcome.reason === "superseded")
|
|
1362
1382
|
console.error(`! manager instance ${iid} was not deregistered: its registration moved while this stop ran, so another incarnation owns it now - leaving it alone`);
|
|
1383
|
+
else if (outcome.reason === "registration-in-flight")
|
|
1384
|
+
console.error(`! manager instance ${iid} was not deregistered: a registration is still in flight (governance slot held at the live gate generation) - leaving it alone`);
|
|
1363
1385
|
// `absent` is silent: there was nothing registered to remove, which is not news at shutdown.
|
|
1364
1386
|
}
|
|
1365
1387
|
catch (e) {
|
|
@@ -4431,8 +4453,9 @@ export class Manager {
|
|
|
4431
4453
|
* `gone` into `unestablishable`). Live / unknown / unestablishable / wrong-op-kind stay loud
|
|
4432
4454
|
* refusals. No TTL: request-ingress has no process-epoch fence, so a clock would either let a
|
|
4433
4455
|
* superseded serve credential keep consuming ingress or just rename the freeze. An open gate
|
|
4434
|
-
* is a no-op (the successor's own freeze comes next).
|
|
4435
|
-
|
|
4456
|
+
* is a no-op (the successor's own freeze comes next). A committed spec under that freeze is
|
|
4457
|
+
* finished at the committed registrationRevision; only a definite no-commit abort-reopens. */
|
|
4458
|
+
async healFrozenRegistrationGate(authKv, instanceId, auth, recordsKv) {
|
|
4436
4459
|
const key = epgateKey(MANAGER_ENDPOINT, instanceId);
|
|
4437
4460
|
const entry = await authKv.get(key);
|
|
4438
4461
|
if (!entry || entry.operation !== "PUT")
|
|
@@ -4449,6 +4472,7 @@ export class Manager {
|
|
|
4449
4472
|
probeHolder: makeManagerHolderLivenessProbe({ space: this.space, servers, auth, log }),
|
|
4450
4473
|
evict: makeManagerEndpointEvictor({ space: this.space, servers, auth, log }),
|
|
4451
4474
|
log,
|
|
4475
|
+
recordsKv,
|
|
4452
4476
|
});
|
|
4453
4477
|
}
|
|
4454
4478
|
catch (e) {
|
|
@@ -4464,7 +4488,7 @@ export class Manager {
|
|
|
4464
4488
|
}
|
|
4465
4489
|
throw e;
|
|
4466
4490
|
}
|
|
4467
|
-
console.error(`✓ boot self-heal: ${MANAGER_ENDPOINT}/${instanceId} registration gate reopened at generation ${report.reopenedAtGeneration} (processEpoch
|
|
4491
|
+
console.error(`✓ boot self-heal: ${MANAGER_ENDPOINT}/${instanceId} registration gate reopened at generation ${report.reopenedAtGeneration} (processEpoch ${report.after.processEpoch}, registrationRevision ${report.after.registrationRevision}; freeze-holder gone, sweepComplete=true). Continuing the normal takeover.`);
|
|
4468
4492
|
}
|
|
4469
4493
|
/** P2 item 1: register the manager as an ordinary v0.4 `service` endpoint and serve its typed
|
|
4470
4494
|
* command surface on the ep rails - since 1d the manager's ONLY control door. On an AUTH mesh
|
|
@@ -4596,8 +4620,9 @@ export class Manager {
|
|
|
4596
4620
|
// when the freeze-holder is gone. Complete that SAME op (abort-reopen) on independent
|
|
4597
4621
|
// holder-gone evidence BEFORE this incarnation freezes a new one. Auth only: the
|
|
4598
4622
|
// CONNZ oracle rides delivery-admin, which an open mesh does not have.
|
|
4599
|
-
if (auth)
|
|
4600
|
-
await this.healFrozenRegistrationGate(authKv, iid, auth);
|
|
4623
|
+
if (auth) {
|
|
4624
|
+
await this.healFrozenRegistrationGate(authKv, iid, auth, recordsKv);
|
|
4625
|
+
}
|
|
4601
4626
|
// P2 item 3 (slice 3a): on an AUTH mesh a RE-registration (restart of the persisted instanceId)
|
|
4602
4627
|
// must VERIFY-EVICT the superseded serve family BEFORE the epoch advances (§13.1 "old authority
|
|
4603
4628
|
// dies before new authority is visible"). Inject the SCOPED delivery-admin evictor; the OPEN
|
|
@@ -5608,7 +5633,7 @@ export class Manager {
|
|
|
5608
5633
|
this.pendingTurns.set(goalId, pending);
|
|
5609
5634
|
this.ensureTurnSweep();
|
|
5610
5635
|
const acceptance = { name: a.name, owner: t.owner, actor: t.actor, uid: t.lifecycleUid, goalId, fingerprint, deadlineAt, executor };
|
|
5611
|
-
this.turnAcceptances.set(goalId, { acceptance });
|
|
5636
|
+
this.turnAcceptances.set(goalId, { acceptance, ref });
|
|
5612
5637
|
this.emitGoalProgress(ref, executor.epoch, { phase: "relayed" });
|
|
5613
5638
|
return acceptance;
|
|
5614
5639
|
}
|
|
@@ -5662,11 +5687,22 @@ export class Manager {
|
|
|
5662
5687
|
// reporting failure for work the run already has. The answer the first reply carried is
|
|
5663
5688
|
// served again instead, to the addressee it was addressed to and nobody else.
|
|
5664
5689
|
const settled = this.turnAcceptances.get(goalId);
|
|
5665
|
-
if (settled
|
|
5690
|
+
if (settled !== undefined) {
|
|
5666
5691
|
const a = settled.acceptance;
|
|
5667
5692
|
if (a.owner !== c.owner || a.actor !== c.actor || a.uid !== c.uid)
|
|
5668
5693
|
throw new EpEnvelopeError("permission-denied", `turn "${goalId}" was addressed to ${a.owner}.${a.actor}/${a.uid}; a yield is the addressee's own (SPEC 13.6)`);
|
|
5669
|
-
|
|
5694
|
+
if (settled.settled !== undefined)
|
|
5695
|
+
return { goalId, state: settled.settled.state };
|
|
5696
|
+
// The in-memory answer is not yet remembered (pending deleted before the terminal CAS,
|
|
5697
|
+
// or a concurrent sweep dropped `settled` while the commit was still in flight). The
|
|
5698
|
+
// durable terminal is the one the run already has; name it, never `not-found`.
|
|
5699
|
+
if (settled.ref !== undefined) {
|
|
5700
|
+
const ended = await readGoalResult(gw.ctx, settled.ref);
|
|
5701
|
+
if (ended !== undefined) {
|
|
5702
|
+
this.rememberSettledTurn(goalId, ended.state, ended.ts);
|
|
5703
|
+
return { goalId, state: ended.state };
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5670
5706
|
}
|
|
5671
5707
|
throw new EpEnvelopeError("not-found", `no pending turn "${goalId}" on this manager`);
|
|
5672
5708
|
}
|
|
@@ -5704,6 +5740,7 @@ export class Manager {
|
|
|
5704
5740
|
this.emitGoalProgress(p.ref, epoch, { phase: "terminal", state: fact.state, ...(fact.data !== undefined ? { data: fact.data } : {}) });
|
|
5705
5741
|
await clearGoalIndex(gw.ctx, p.ref);
|
|
5706
5742
|
this.pendingTurns.delete(goalId);
|
|
5743
|
+
this.markTurnLatchDropped(goalId, at);
|
|
5707
5744
|
this.rememberSettledTurn(goalId, fact.state, at);
|
|
5708
5745
|
this.maybeStopTurnSweep();
|
|
5709
5746
|
return { goalId, state: fact.state };
|
|
@@ -5718,17 +5755,18 @@ export class Manager {
|
|
|
5718
5755
|
return;
|
|
5719
5756
|
if (!this.pendingTurns.delete(p.goalId))
|
|
5720
5757
|
return;
|
|
5758
|
+
this.markTurnLatchDropped(p.goalId);
|
|
5721
5759
|
const epoch = this.serviceServe?.grant.epoch ?? 0;
|
|
5722
5760
|
try {
|
|
5723
5761
|
await this.assertGoalWriterEpochCurrent(epoch);
|
|
5724
5762
|
const { fact } = await commitGoalResult(gw.ctx, { ref: p.ref, now: Date.now(), cause: "deny", denial: { kind: "hold-expired", token: p.holdToken }, data: { reason: "turn-deadline", ...(p.seatDiedAt !== undefined ? { agentDownAt: p.seatDiedAt } : {}), ...(p.handoffFrom !== undefined ? { handoffFrom: p.handoffFrom } : {}) }, committer: { instanceId: this.managerInstanceId, epoch } });
|
|
5725
5763
|
this.emitGoalProgress(p.ref, epoch, { phase: "terminal", state: fact.state, ...(fact.data !== undefined ? { data: fact.data } : {}) });
|
|
5726
|
-
|
|
5727
|
-
//
|
|
5728
|
-
// first,
|
|
5729
|
-
//
|
|
5730
|
-
// `not-found` for the last expired turn, which the seat reads as "drop it".
|
|
5764
|
+
// Remember BEFORE the index clear: a late yield that arrives after the pending latch
|
|
5765
|
+
// (the delete at the top of this function) and after this CAS has to name this terminal.
|
|
5766
|
+
// Clearing first, then remembering, left a window where maybeStopTurnSweep saw no pending
|
|
5767
|
+
// turn and no settled answer, wiped the acceptance, and rememberSettledTurn no-op'd.
|
|
5731
5768
|
this.rememberSettledTurn(p.goalId, fact.state, Date.now());
|
|
5769
|
+
await clearGoalIndex(gw.ctx, p.ref);
|
|
5732
5770
|
}
|
|
5733
5771
|
catch (e) {
|
|
5734
5772
|
console.error(`! turn deadline terminal for ${p.goalId}: ${e.message}`);
|
|
@@ -5775,16 +5813,27 @@ export class Manager {
|
|
|
5775
5813
|
return;
|
|
5776
5814
|
entry.settled = { state, at };
|
|
5777
5815
|
}
|
|
5816
|
+
/** Stamp when the pending latch dropped, so an unsettled leftover ages off that instant rather
|
|
5817
|
+
* than the original deadline. Idempotent: the first drop is the one the retry window starts at. */
|
|
5818
|
+
markTurnLatchDropped(goalId, at = Date.now()) {
|
|
5819
|
+
const entry = this.turnAcceptances.get(goalId);
|
|
5820
|
+
if (entry === undefined || entry.leftoverSince !== undefined)
|
|
5821
|
+
return;
|
|
5822
|
+
entry.leftoverSince = at;
|
|
5823
|
+
}
|
|
5778
5824
|
/** Stop the sweep only when it has nothing left to do. It drives elapsed turns to their deadline
|
|
5779
5825
|
* terminals AND drops settled answers once their retry window has passed, so a stop while the
|
|
5780
5826
|
* second is still owed would leave one entry per turn for the process lifetime. */
|
|
5781
5827
|
maybeStopTurnSweep() {
|
|
5782
5828
|
if (this.pendingTurns.size > 0)
|
|
5783
5829
|
return;
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5830
|
+
// An acceptance with no `settled` yet is the in-flight deadline commit: pending was the
|
|
5831
|
+
// latch and is already gone, the terminal CAS has not returned, and a concurrent sweep
|
|
5832
|
+
// tick used to clear the map here, so rememberSettledTurn no-op'd and a late yield heard
|
|
5833
|
+
// `not-found` for a deny the run already has. Prune is the only deleter; stop only when
|
|
5834
|
+
// it has left nothing.
|
|
5835
|
+
if (this.turnAcceptances.size > 0)
|
|
5836
|
+
return;
|
|
5788
5837
|
this.stopTurnSweep();
|
|
5789
5838
|
}
|
|
5790
5839
|
stopTurnSweep() {
|
|
@@ -5798,9 +5847,17 @@ export class Manager {
|
|
|
5798
5847
|
async sweepTurnDeadlines() {
|
|
5799
5848
|
const now = Date.now();
|
|
5800
5849
|
// A settled answer is kept only as long as a lost reply could still be retried under it.
|
|
5801
|
-
|
|
5802
|
-
|
|
5850
|
+
// An UNSETTLED acceptance whose pending latch is already gone is the in-flight (or failed)
|
|
5851
|
+
// deadline commit: keep it for the same window so a late yield can still read the durable
|
|
5852
|
+
// terminal, then drop it so a commit that never remembered cannot pin the sweep forever.
|
|
5853
|
+
for (const [goalId, e] of [...this.turnAcceptances.entries()]) {
|
|
5854
|
+
if (this.pendingTurns.has(goalId))
|
|
5855
|
+
continue;
|
|
5856
|
+
// Unsettled leftovers age off the latch-drop stamp, never `deadlineAt`.
|
|
5857
|
+
const at = e.settled?.at ?? e.leftoverSince;
|
|
5858
|
+
if (at !== undefined && now - at >= TURN_ANSWER_RETENTION_MS)
|
|
5803
5859
|
this.turnAcceptances.delete(goalId);
|
|
5860
|
+
}
|
|
5804
5861
|
// The prune above is the one event after which the sweep may have nothing left to do, and
|
|
5805
5862
|
// the settle-time callers cannot see it: they run when an answer has just been remembered.
|
|
5806
5863
|
this.maybeStopTurnSweep();
|
|
@@ -5881,6 +5938,7 @@ export class Manager {
|
|
|
5881
5938
|
fingerprint: spec.value.fingerprint, deadlineAt: p.deadlineAt,
|
|
5882
5939
|
executor: { lifecycleUid: this.managerInstanceId, epoch: p.holdEpoch },
|
|
5883
5940
|
},
|
|
5941
|
+
ref: p.ref,
|
|
5884
5942
|
});
|
|
5885
5943
|
// NO LIVENESS VERDICT AT BOOT. The agents map is empty here: seats are re-registered later,
|
|
5886
5944
|
// by the resume path, so `not in the map` at this moment means "not yet re-registered", never
|