@felan-ai/daemon 0.7.1 → 0.7.3
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/daemon/broker.d.ts.map +1 -1
- package/dist/daemon/broker.js +23 -2
- package/dist/daemon/broker.js.map +1 -1
- package/dist/daemon/message-sanitize.d.ts +12 -0
- package/dist/daemon/message-sanitize.d.ts.map +1 -0
- package/dist/daemon/message-sanitize.js +119 -0
- package/dist/daemon/message-sanitize.js.map +1 -0
- package/dist/daemon/projection.d.ts +2 -0
- package/dist/daemon/projection.d.ts.map +1 -1
- package/dist/daemon/projection.js +112 -74
- package/dist/daemon/projection.js.map +1 -1
- package/dist/daemon/registry.d.ts +3 -0
- package/dist/daemon/registry.d.ts.map +1 -1
- package/dist/daemon/registry.js +115 -26
- package/dist/daemon/registry.js.map +1 -1
- package/dist/daemon/snapshot-bounds.d.ts +7 -0
- package/dist/daemon/snapshot-bounds.d.ts.map +1 -0
- package/dist/daemon/snapshot-bounds.js +85 -0
- package/dist/daemon/snapshot-bounds.js.map +1 -0
- package/dist/remote/content.d.ts.map +1 -1
- package/dist/remote/content.js +17 -9
- package/dist/remote/content.js.map +1 -1
- package/package.json +3 -3
package/dist/daemon/registry.js
CHANGED
|
@@ -16,6 +16,7 @@ import { sessionTip } from './session-cursor.js';
|
|
|
16
16
|
import { shutdownStep } from './shutdown-error.js';
|
|
17
17
|
import { HostWorkers } from './host-workers.js';
|
|
18
18
|
import { assertStagedWorker } from './worker.js';
|
|
19
|
+
export const PROJECTION_CLEANUP_LIMIT_MS = 30_000;
|
|
19
20
|
const nonRunMutations = new Set(['set_model', 'set_thinking', 'session.rename', 'session.entry.label', 'session.branch.select', 'session.export', 'command.invoke', 'shell.execute', 'processes.write', 'processes.stop', 'steer', 'follow_up', 'queue.clear', 'executions.steer', 'executions.cancel', 'ui.action']);
|
|
20
21
|
class RegistryOpeningCancelled extends Error {
|
|
21
22
|
}
|
|
@@ -33,6 +34,7 @@ export class SessionRegistry {
|
|
|
33
34
|
copyClaims = new Set();
|
|
34
35
|
reservations = new Map();
|
|
35
36
|
lifecycleWriters = new Set();
|
|
37
|
+
lateWriters = new Map();
|
|
36
38
|
reloadAttachments = new Map();
|
|
37
39
|
invalidationListeners = new Set();
|
|
38
40
|
observers = new Map();
|
|
@@ -108,7 +110,7 @@ export class SessionRegistry {
|
|
|
108
110
|
if (session.deletedAt !== undefined)
|
|
109
111
|
throw registryError('not_found', 'Session was deleted.');
|
|
110
112
|
if (this.entries.get(id)?.projectionFailed)
|
|
111
|
-
throw registryError('unavailable', 'Session projection failed;
|
|
113
|
+
throw registryError('unavailable', 'Session projection failed; it reopens once cleanup finishes, try again shortly.');
|
|
112
114
|
if (this.reservations.has(session.piPath) && !activatingBinding)
|
|
113
115
|
throw registryError('busy', 'Session has a lifecycle operation in progress.');
|
|
114
116
|
if (session.state !== 'open' || this.entries.get(id)?.disposing || this.entries.get(id)?.cancelling || this.closing.has(id) || this.closing.has(session.root) || this.copyClaims.has(session.piPath))
|
|
@@ -175,9 +177,9 @@ export class SessionRegistry {
|
|
|
175
177
|
async activeWork() {
|
|
176
178
|
this.options.owner.assertCurrent();
|
|
177
179
|
const interruptible = await this.processes.interruptibleCount();
|
|
178
|
-
const busy = [...this.entries
|
|
180
|
+
const busy = [...this.entries].filter(([id, entry]) => this.lateWriters.has(id) || !entry.binding.runtime.session.isIdle || entry.dispatches.size || entry.writers.size
|
|
179
181
|
|| entry.host?.hasPendingWork() || entry.host?.hasActiveJobs() || [...entry.services].some(service => service.busy())).length;
|
|
180
|
-
return busy + interruptible;
|
|
182
|
+
return busy + [...this.lateWriters.keys()].filter(id => !this.entries.has(id)).length + interruptible;
|
|
181
183
|
}
|
|
182
184
|
create(request) {
|
|
183
185
|
this.assertNewWork();
|
|
@@ -270,6 +272,8 @@ export class SessionRegistry {
|
|
|
270
272
|
throw registryError('not_found', 'Session is not registered.');
|
|
271
273
|
if (stored.deletedAt !== undefined)
|
|
272
274
|
throw registryError('not_found', 'Session was deleted.');
|
|
275
|
+
if (this.lateWriters.has(id))
|
|
276
|
+
throw registryError('busy', 'Session transcript already has a writer.');
|
|
273
277
|
if (this.reloadAttachments.has(id) && this.opening.has(id))
|
|
274
278
|
return this.opening.get(id);
|
|
275
279
|
if (this.closing.has(id))
|
|
@@ -282,6 +286,8 @@ export class SessionRegistry {
|
|
|
282
286
|
const pending = this.opening.get(id);
|
|
283
287
|
if (pending)
|
|
284
288
|
return pending;
|
|
289
|
+
if (this.pathClaims.has(stored.piPath))
|
|
290
|
+
throw registryError('busy', 'Session transcript already has a writer.');
|
|
285
291
|
const reopening = stored.state === 'closed';
|
|
286
292
|
if (reopening)
|
|
287
293
|
this.options.store.updateSession(id, { state: 'open' });
|
|
@@ -411,7 +417,7 @@ export class SessionRegistry {
|
|
|
411
417
|
const entry = this.require(id);
|
|
412
418
|
const attachment = randomUUID();
|
|
413
419
|
if (entry.projectionFailed)
|
|
414
|
-
throw registryError('unavailable', 'Session projection failed;
|
|
420
|
+
throw registryError('unavailable', 'Session projection failed; cleanup is still in progress.');
|
|
415
421
|
const snapshot = entry.projection.snapshot();
|
|
416
422
|
entry.attachments.set(attachment, { ...metadata });
|
|
417
423
|
return { attachment, snapshot };
|
|
@@ -761,6 +767,8 @@ export class SessionRegistry {
|
|
|
761
767
|
throw registryError('busy', 'Wait for the current run to finish before reloading.');
|
|
762
768
|
if (this.opening.has(id) || entry?.disposing || entry?.cancelling || entry?.writers.size)
|
|
763
769
|
throw registryError('busy', 'Session has a lifecycle operation in progress.');
|
|
770
|
+
if (!entry && this.pathClaims.has(stored.piPath))
|
|
771
|
+
throw registryError('busy', 'Session transcript already has a writer.');
|
|
764
772
|
const token = Symbol('reload');
|
|
765
773
|
this.reservations.set(stored.piPath, { token, id });
|
|
766
774
|
const attachments = entry?.attachments ?? new Map();
|
|
@@ -1057,23 +1065,27 @@ export class SessionRegistry {
|
|
|
1057
1065
|
// Host shutdown cancels its full descendant tree and waits beyond terminal status records.
|
|
1058
1066
|
const entry = this.entries.get(id);
|
|
1059
1067
|
this.settleInteractions(id, 'close');
|
|
1060
|
-
if (entry)
|
|
1068
|
+
if (entry && !entry.projectionFailed)
|
|
1061
1069
|
await this.cancelViews(id);
|
|
1062
|
-
|
|
1070
|
+
if (!entry?.projectionFailed)
|
|
1071
|
+
await entry?.host?.shutdown();
|
|
1063
1072
|
for (const child of descendants)
|
|
1064
1073
|
await this.close(child.id);
|
|
1065
1074
|
if (entry) {
|
|
1066
|
-
entry.
|
|
1067
|
-
|
|
1075
|
+
if (!entry.projectionFailed) {
|
|
1076
|
+
entry.runAbort?.abort();
|
|
1077
|
+
await entry.cancelling;
|
|
1078
|
+
}
|
|
1068
1079
|
await entry.projectionCancellation;
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1080
|
+
if (!entry.projectionFailed) {
|
|
1081
|
+
await entry.binding.runtime.session.abort();
|
|
1082
|
+
await this.processes.cancelForeground(id);
|
|
1083
|
+
await Promise.all(entry.writers);
|
|
1073
1084
|
entry.projection.settleRun(this.options.store.sessionMetadata(id).run.id);
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1085
|
+
await this.processes.settle(stored.root, false, id === stored.root ? undefined : new Set([id]));
|
|
1086
|
+
for (const service of entry.services)
|
|
1087
|
+
await this.processes.withCleanup(id, () => service.settle());
|
|
1088
|
+
}
|
|
1077
1089
|
await this.disposeEntry(id, true);
|
|
1078
1090
|
}
|
|
1079
1091
|
else {
|
|
@@ -1109,7 +1121,7 @@ export class SessionRegistry {
|
|
|
1109
1121
|
if (!options.interrupt || this.failure)
|
|
1110
1122
|
throw error;
|
|
1111
1123
|
});
|
|
1112
|
-
await this.join([hostWorkers, ...cancellations, shutdownStep('registry-pending', () => this.join([...this.opening.values(), closing, ...this.lifecycleWriters], undefined, true))]);
|
|
1124
|
+
await this.join([hostWorkers, ...cancellations, shutdownStep('registry-pending', () => this.join([...this.opening.values(), closing, ...this.lifecycleWriters, ...this.lateWriters.values()], undefined, true))]);
|
|
1113
1125
|
if (this.failure)
|
|
1114
1126
|
await shutdownStep('registry-fenced', async () => { await this.failureSettlement; throw this.failure; });
|
|
1115
1127
|
if (options.interrupt) {
|
|
@@ -1160,6 +1172,7 @@ export class SessionRegistry {
|
|
|
1160
1172
|
this.options.store.interruptSessionRun(id, run); }, id);
|
|
1161
1173
|
this.shutdownInterruptions.delete(id);
|
|
1162
1174
|
}
|
|
1175
|
+
await shutdownStep('registry-pending', () => this.join([...this.lateWriters.values()]));
|
|
1163
1176
|
this.retainedServices.clear();
|
|
1164
1177
|
this.lossUnsubscribe();
|
|
1165
1178
|
this.listeners.clear();
|
|
@@ -1607,15 +1620,21 @@ export class SessionRegistry {
|
|
|
1607
1620
|
this.options.owner.assertCurrent();
|
|
1608
1621
|
const stored = this.options.store.sessionMetadata(id);
|
|
1609
1622
|
const closed = close || this.closes.has(id);
|
|
1610
|
-
if (entry.projectionFailed)
|
|
1611
|
-
this.
|
|
1623
|
+
if (entry.projectionFailed) {
|
|
1624
|
+
this.interruptRun(id);
|
|
1625
|
+
const commit = this.options.store.transitionSessionForSafety(id, closed ? 'closed' : 'evicted');
|
|
1626
|
+
if (commit.revisions[id] !== undefined)
|
|
1627
|
+
this.publish({ v: 1, session: id, rev: commit.value.revision,
|
|
1628
|
+
change: { type: 'metadata.updated', title: commit.value.title, state: commit.value.state, residency: commit.value.residency } });
|
|
1629
|
+
}
|
|
1612
1630
|
else
|
|
1613
1631
|
entry.projection.transitionForSafety(closed ? 'closed' : 'evicted');
|
|
1614
1632
|
entry.projection.dispose();
|
|
1615
1633
|
this.entries.delete(id);
|
|
1616
1634
|
if (!preserveView)
|
|
1617
1635
|
this.listeners.delete(id);
|
|
1618
|
-
this.
|
|
1636
|
+
if (!this.lateWriters.has(id))
|
|
1637
|
+
this.pathClaims.delete(stored.piPath);
|
|
1619
1638
|
if (!close && entry.services.size)
|
|
1620
1639
|
this.retainedServices.set(id, entry.services);
|
|
1621
1640
|
})();
|
|
@@ -1628,6 +1647,14 @@ export class SessionRegistry {
|
|
|
1628
1647
|
throw error;
|
|
1629
1648
|
}
|
|
1630
1649
|
}
|
|
1650
|
+
interruptRun(id) {
|
|
1651
|
+
const run = this.options.store.sessionMetadata(id).run;
|
|
1652
|
+
if (!run.id || !['running', 'waiting', 'cancelling'].includes(run.state))
|
|
1653
|
+
return;
|
|
1654
|
+
const commit = this.options.store.interruptSessionRun(id, run.id);
|
|
1655
|
+
if (commit.revisions[id] !== undefined)
|
|
1656
|
+
this.publish({ v: 1, session: id, rev: commit.value.revision, change: { type: 'run.updated', run: commit.value.run } });
|
|
1657
|
+
}
|
|
1631
1658
|
projectionFailed(session, error) {
|
|
1632
1659
|
this.diagnostic({ scope: 'projection', session, error });
|
|
1633
1660
|
if (!this.projectionIntegrityIntact(error))
|
|
@@ -1635,19 +1662,29 @@ export class SessionRegistry {
|
|
|
1635
1662
|
const entry = this.entries.get(session);
|
|
1636
1663
|
if (!entry || entry.projectionFailed)
|
|
1637
1664
|
return;
|
|
1665
|
+
const path = this.options.store.sessionMetadata(session).piPath;
|
|
1638
1666
|
entry.projectionFailed = true;
|
|
1639
1667
|
this.processes.seal(session);
|
|
1668
|
+
let expired = false;
|
|
1669
|
+
const failed = (cleanup) => {
|
|
1670
|
+
this.diagnostic({ scope: 'registry', session, error: cleanup });
|
|
1671
|
+
this.cancellationFailures.add(cleanup);
|
|
1672
|
+
this.projectionIntegrityIntact(cleanup);
|
|
1673
|
+
};
|
|
1640
1674
|
const attempt = async (operation) => {
|
|
1675
|
+
if (expired || this.entries.get(session) !== entry)
|
|
1676
|
+
return;
|
|
1641
1677
|
try {
|
|
1642
1678
|
await operation();
|
|
1643
1679
|
}
|
|
1644
1680
|
catch (cleanup) {
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1681
|
+
try {
|
|
1682
|
+
failed(cleanup);
|
|
1683
|
+
}
|
|
1684
|
+
catch { }
|
|
1648
1685
|
}
|
|
1649
1686
|
};
|
|
1650
|
-
|
|
1687
|
+
const cancellation = Promise.resolve().then(async () => {
|
|
1651
1688
|
await attempt(() => entry.projection.dispose());
|
|
1652
1689
|
await attempt(() => entry.runAbort?.abort());
|
|
1653
1690
|
for (const controller of entry.viewActions ?? [])
|
|
@@ -1677,7 +1714,59 @@ export class SessionRegistry {
|
|
|
1677
1714
|
throw new Error('Projection-failed session has not settled.');
|
|
1678
1715
|
transitionRun('idle');
|
|
1679
1716
|
});
|
|
1680
|
-
}).
|
|
1717
|
+
}).catch(cleanup => { try {
|
|
1718
|
+
failed(cleanup);
|
|
1719
|
+
}
|
|
1720
|
+
catch { } });
|
|
1721
|
+
let timer;
|
|
1722
|
+
const timeout = new Error(`Projection-failure cleanup timed out after ${PROJECTION_CLEANUP_LIMIT_MS} milliseconds.`);
|
|
1723
|
+
const deadline = new Promise(resolve => {
|
|
1724
|
+
timer = setTimeout(() => {
|
|
1725
|
+
expired = true;
|
|
1726
|
+
const failures = [];
|
|
1727
|
+
const late = this.join([cancellation, ...(entry.cancelling ? [entry.cancelling] : [])], failures).then(async () => {
|
|
1728
|
+
do {
|
|
1729
|
+
await this.join([...entry.writers], failures);
|
|
1730
|
+
} while (entry.writers.size);
|
|
1731
|
+
await this.join([eviction, ...(this.closes.has(session) ? [this.closes.get(session)] : []), ...(entry.disposing ? [entry.disposing] : [])], failures);
|
|
1732
|
+
for (const failure of failures) {
|
|
1733
|
+
try {
|
|
1734
|
+
failed(failure);
|
|
1735
|
+
}
|
|
1736
|
+
catch { }
|
|
1737
|
+
}
|
|
1738
|
+
}).finally(() => {
|
|
1739
|
+
this.lateWriters.delete(session);
|
|
1740
|
+
this.cancellationFailures.delete(timeout);
|
|
1741
|
+
if (!this.entries.has(session))
|
|
1742
|
+
this.pathClaims.delete(path);
|
|
1743
|
+
});
|
|
1744
|
+
this.lateWriters.set(session, late);
|
|
1745
|
+
try {
|
|
1746
|
+
failed(timeout);
|
|
1747
|
+
}
|
|
1748
|
+
catch { }
|
|
1749
|
+
finally {
|
|
1750
|
+
resolve();
|
|
1751
|
+
}
|
|
1752
|
+
}, PROJECTION_CLEANUP_LIMIT_MS);
|
|
1753
|
+
});
|
|
1754
|
+
entry.projectionCancellation = Promise.race([cancellation, deadline]).then(() => {
|
|
1755
|
+
if (!this.failure)
|
|
1756
|
+
try {
|
|
1757
|
+
this.interruptRun(session);
|
|
1758
|
+
}
|
|
1759
|
+
catch (cleanup) {
|
|
1760
|
+
this.fence(cleanup);
|
|
1761
|
+
throw cleanup;
|
|
1762
|
+
}
|
|
1763
|
+
}).finally(() => { clearTimeout(timer); delete entry.projectionCancellation; });
|
|
1764
|
+
const eviction = entry.projectionCancellation.then(async () => {
|
|
1765
|
+
if (!this.failure && !this.stopping && !this.closing.has(session) && this.entries.get(session) === entry)
|
|
1766
|
+
await this.disposeEntry(session, false);
|
|
1767
|
+
});
|
|
1768
|
+
this.lifecycleWriters.add(eviction);
|
|
1769
|
+
void eviction.catch(cleanup => this.diagnostic({ scope: 'registry', session, error: cleanup })).finally(() => this.lifecycleWriters.delete(eviction));
|
|
1681
1770
|
}
|
|
1682
1771
|
projectionIntegrityIntact(error) {
|
|
1683
1772
|
if (error instanceof AggregateError) {
|
|
@@ -1765,10 +1854,10 @@ export class SessionRegistry {
|
|
|
1765
1854
|
throw new AggregateError(failures, 'Registry writers did not settle cleanly; ownership must remain held.');
|
|
1766
1855
|
}
|
|
1767
1856
|
pendingWriters() {
|
|
1768
|
-
return [...this.opening.values(), ...this.lifecycleWriters, ...[...this.entries.values()].flatMap(entry => [...entry.writers, ...(entry.cancelling ? [entry.cancelling] : []), ...(entry.projectionCancellation ? [entry.projectionCancellation] : [])])];
|
|
1857
|
+
return [...this.opening.values(), ...this.lifecycleWriters, ...this.lateWriters.values(), ...[...this.entries.values()].flatMap(entry => [...entry.writers, ...(entry.cancelling ? [entry.cancelling] : []), ...(entry.projectionCancellation ? [entry.projectionCancellation] : [])])];
|
|
1769
1858
|
}
|
|
1770
1859
|
hasPendingWriters() {
|
|
1771
|
-
return this.opening.size > 0 || this.lifecycleWriters.size > 0 || [...this.entries.values()].some(entry => entry.writers.size > 0 || entry.cancelling !== undefined || entry.projectionCancellation !== undefined);
|
|
1860
|
+
return this.opening.size > 0 || this.lifecycleWriters.size > 0 || this.lateWriters.size > 0 || [...this.entries.values()].some(entry => entry.writers.size > 0 || entry.cancelling !== undefined || entry.projectionCancellation !== undefined);
|
|
1772
1861
|
}
|
|
1773
1862
|
async join(operations, failures, allowCancelled = false) {
|
|
1774
1863
|
const errors = failures ?? [];
|