@animalabs/agent-framework 0.30.0 → 0.31.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/README.md +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +129 -92
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1115 -704
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +2 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +54 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1145 -723
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +2 -1
- package/src/types/framework.ts +55 -15
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
|
@@ -49,6 +49,54 @@ function writeIdOf(m) {
|
|
|
49
49
|
const writeId = m.metadata?.deferredWriteId;
|
|
50
50
|
return typeof writeId === 'string' ? writeId : undefined;
|
|
51
51
|
}
|
|
52
|
+
function keysOf(rows) {
|
|
53
|
+
const keys = { ids: [], writeIds: [] };
|
|
54
|
+
for (const m of rows) {
|
|
55
|
+
const writeId = m.writeId ?? writeIdOf(m);
|
|
56
|
+
if (writeId !== undefined)
|
|
57
|
+
keys.writeIds.push(writeId);
|
|
58
|
+
else if (m.id !== undefined)
|
|
59
|
+
keys.ids.push(m.id);
|
|
60
|
+
}
|
|
61
|
+
return keys;
|
|
62
|
+
}
|
|
63
|
+
function keyedBy(keys) {
|
|
64
|
+
const ids = new Set(keys.flatMap((k) => k.ids));
|
|
65
|
+
const writeIds = new Set(keys.flatMap((k) => k.writeIds));
|
|
66
|
+
return (m) => {
|
|
67
|
+
const writeId = m.writeId ?? writeIdOf(m);
|
|
68
|
+
return writeId !== undefined ? writeIds.has(writeId) : m.id !== undefined && ids.has(m.id);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const isStrings = (value) => Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
72
|
+
const isRowKeys = (value) => typeof value === 'object' && value !== null && isStrings(value.ids) && isStrings(value.writeIds);
|
|
73
|
+
function isReleaseRecord(value) {
|
|
74
|
+
if (!isRowKeys(value))
|
|
75
|
+
return false;
|
|
76
|
+
const record = value;
|
|
77
|
+
return typeof record.total === 'number' && typeof record.truncated === 'number' && isRowKeys(record.report);
|
|
78
|
+
}
|
|
79
|
+
function releaseRecordsOf(rows, epochId) {
|
|
80
|
+
const records = [];
|
|
81
|
+
for (const m of rows) {
|
|
82
|
+
const meta = m.metadata;
|
|
83
|
+
if (meta?.kind !== 'tune-out-cancel' || meta.epochId !== epochId)
|
|
84
|
+
continue;
|
|
85
|
+
if (isReleaseRecord(meta.released) && !records.some((r) => JSON.stringify(r) === JSON.stringify(meta.released))) {
|
|
86
|
+
records.push(meta.released);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return records;
|
|
90
|
+
}
|
|
91
|
+
function truncationNote(records) {
|
|
92
|
+
const truncated = records.reduce((n, r) => n + r.truncated, 0);
|
|
93
|
+
if (truncated === 0)
|
|
94
|
+
return '';
|
|
95
|
+
const total = records.reduce((n, r) => n + r.total, 0);
|
|
96
|
+
return records.length === 1
|
|
97
|
+
? ` ${truncated} of ${total} diverted messages, the oldest, weren't delivered raw to the resident.`
|
|
98
|
+
: ` ${truncated} of ${total} diverted messages across ${records.length} dumps, the oldest in each dump that left any out, weren't delivered raw to the resident.`;
|
|
99
|
+
}
|
|
52
100
|
/**
|
|
53
101
|
* Whether a held message is one of this channel's: by its channel id, and by
|
|
54
102
|
* its server where it records one, since two servers can register the same
|
|
@@ -61,7 +109,7 @@ function inChannelOf(serverId, channelId) {
|
|
|
61
109
|
return (meta?.channelId ?? channelId) === channelId && (meta?.serverId ?? serverId) === serverId;
|
|
62
110
|
};
|
|
63
111
|
}
|
|
64
|
-
/** How long after an end time
|
|
112
|
+
/** How long after an end time's or a wake budget's cancel threw the cancel is tried again. */
|
|
65
113
|
const EXPIRY_RETRY_MS = 60_000;
|
|
66
114
|
/** Ends a backlog line cut to fit in one of the residents' messages. */
|
|
67
115
|
const CUT_MARK = ` [${NOTICE_TAG.cutToFit}; the whole message is in the channel's history]`;
|
|
@@ -157,8 +205,6 @@ export class TuneOutCoordinator {
|
|
|
157
205
|
lateForks = new WeakSet();
|
|
158
206
|
/** The all-except scope's durable record, cached (undefined = not read yet). */
|
|
159
207
|
scopeCache;
|
|
160
|
-
/** The epoch whose release this process delivered and whose hold is not yet recorded as cleared. */
|
|
161
|
-
scopeReleased;
|
|
162
208
|
/** Timer key for the scope's cadence and expiry. */
|
|
163
209
|
static SCOPE_KEY = '\0all-except';
|
|
164
210
|
constructor(channelRegistry, serverRegistry, hooks) {
|
|
@@ -207,26 +253,59 @@ export class TuneOutCoordinator {
|
|
|
207
253
|
// An epoch whose deadline passed while the process was down cancels
|
|
208
254
|
// immediately — the resident gets their backlog on the schedule they
|
|
209
255
|
// committed to, not whenever the host happened to restart.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
256
|
+
const inForce = () => this.channelRegistry.getTuneOutState(serverId, channelId)?.params.epochId === params.epochId;
|
|
257
|
+
const expired = params.expiresAtMs !== undefined && params.expiresAtMs <= Date.now();
|
|
258
|
+
if (expired) {
|
|
259
|
+
try {
|
|
260
|
+
this.cancel(serverId, channelId, 'duration', 'duration elapsed');
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
if (!inForce()) {
|
|
265
|
+
console.error(`[tune-out] ${this.shown(serverId, channelId)} reached its end time while the process was down and its cancel ended it, but a step after the end failed:`, err);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
console.error(`[tune-out] ${this.shown(serverId, channelId)} reached its end time while the process was down, and its cancel failed, so it stays tuned out and its cancel is tried again in ${EXPIRY_RETRY_MS / 1000} s:`, err);
|
|
269
|
+
}
|
|
213
270
|
}
|
|
214
271
|
this.startCadence(serverId, channelId, params.cadenceSeconds);
|
|
215
|
-
this.
|
|
272
|
+
const wakeCount = this.channelRegistry.getTuneOutState(serverId, channelId)?.wakeCount ?? 0;
|
|
273
|
+
if (wakeCount > params.maxWakes) {
|
|
274
|
+
this.cancelPastBudget(serverId, channelId, params.maxWakes, 'start');
|
|
275
|
+
if (!inForce())
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
this.armExpiry(serverId, channelId, expired ? { ...params, expiresAtMs: Date.now() + EXPIRY_RETRY_MS } : params);
|
|
280
|
+
}
|
|
216
281
|
if (oldestAnchor === null || params.startedAtSequence < oldestAnchor) {
|
|
217
282
|
oldestAnchor = params.startedAtSequence;
|
|
218
283
|
}
|
|
219
284
|
}
|
|
220
285
|
const scope = this.getAllExcept();
|
|
221
286
|
if (scope) {
|
|
287
|
+
let inForce = true;
|
|
222
288
|
if (scope.params.expiresAtMs <= Date.now()) {
|
|
223
|
-
|
|
289
|
+
try {
|
|
290
|
+
this.cancelAllExcept('duration', 'duration elapsed');
|
|
291
|
+
inForce = false;
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
if (this.getAllExcept()?.params.epochId !== scope.params.epochId) {
|
|
295
|
+
console.error('[tune-out] the all-except hold reached its end time while the process was down and its release ended it, but a step after the end failed:', err);
|
|
296
|
+
inForce = false;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
console.error(`[tune-out] the all-except hold reached its end time while the process was down, and its release failed, so it stays in force and its release is tried again in ${EXPIRY_RETRY_MS / 1000} s:`, err);
|
|
300
|
+
this.armScopeTimers({ ...scope.params, expiresAtMs: Date.now() + EXPIRY_RETRY_MS });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
224
303
|
}
|
|
225
304
|
else {
|
|
226
305
|
this.armScopeTimers(scope.params);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
306
|
+
}
|
|
307
|
+
if (inForce && (oldestAnchor === null || scope.params.startedAtSequence < oldestAnchor)) {
|
|
308
|
+
oldestAnchor = scope.params.startedAtSequence;
|
|
230
309
|
}
|
|
231
310
|
}
|
|
232
311
|
if (oldestAnchor !== null) {
|
|
@@ -418,41 +497,28 @@ export class TuneOutCoordinator {
|
|
|
418
497
|
hasReader() {
|
|
419
498
|
return this.readerAvailable();
|
|
420
499
|
}
|
|
421
|
-
/**
|
|
422
|
-
* End a tune-out: durable flip, system-framed backlog dump into the
|
|
423
|
-
* resident's window (capped), lifecycle notice, one wake for the
|
|
424
|
-
* resident. `subconsciousNote` is the subconscious's own text (from its
|
|
425
|
-
* cancel_tuneout call) — delivered verbatim under its name, never
|
|
426
|
-
* synthesized here.
|
|
427
|
-
*/
|
|
428
500
|
cancel(serverId, channelId, source, reason, subconsciousNote,
|
|
429
501
|
/** `reader: 'forks'`: the reader fork whose note this is. */
|
|
430
502
|
noteAuthor,
|
|
431
503
|
/** The lifecycle state the channel returns to (a tool close ends in 'closed'). */
|
|
432
504
|
nextState = 'open') {
|
|
433
|
-
const
|
|
434
|
-
if (!
|
|
505
|
+
const ending = this.channelRegistry.getTuneOutState(serverId, channelId);
|
|
506
|
+
if (!ending) {
|
|
435
507
|
// Its reader is the resident's tune_out: cancel_tuneout finds the channel first.
|
|
436
508
|
return { ok: false, error: this.notTunedOut(this.quotedShown(serverId, channelId), 'you') };
|
|
437
509
|
}
|
|
438
|
-
|
|
439
|
-
this.divertedSinceInvocation.delete(this.key(serverId, channelId));
|
|
440
|
-
// Backlog: every message held under this epoch. The stored ones come in
|
|
441
|
-
// store order, then those still in the deferred queue (held mid-turn,
|
|
442
|
-
// not yet written), oldest first. That's arrival order, except when a
|
|
443
|
-
// held message stays queued while a later one is stored: a store write
|
|
444
|
-
// refused at a turn-start flush leaves it queued, and that turn's
|
|
445
|
-
// assembly can then store later coalesced messages directly.
|
|
446
|
-
const epochId = ended.params.epochId;
|
|
510
|
+
const epochId = ending.params.epochId;
|
|
447
511
|
const ofEpoch = (m) => m.metadata?.tuneOut?.epochId === epochId;
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
512
|
+
const storedRows = this.hooks.getStoredMessages();
|
|
513
|
+
const queuedRows = this.hooks.getDeferredMessages?.() ?? [];
|
|
514
|
+
const earlier = releaseRecordsOf([...storedRows, ...queuedRows], epochId);
|
|
515
|
+
const dumpedEarlier = keyedBy(earlier);
|
|
516
|
+
const unreleased = (m) => ofEpoch(m) && !dumpedEarlier(m);
|
|
517
|
+
const held = queuedRows.filter(unreleased);
|
|
518
|
+
const backlog = [...storedRows.filter(unreleased), ...held];
|
|
453
519
|
// Whole messages: a cap below one shows none, where slice(-0.5) would
|
|
454
520
|
// start at 0 and take them all.
|
|
455
|
-
const cap = Math.floor(
|
|
521
|
+
const cap = Math.floor(ending.params.backlogCap);
|
|
456
522
|
const authorOf = (m) => suppliedValue(m.metadata?.author?.name ?? m.participant);
|
|
457
523
|
// A line names its author as the resident knows them and marks what a
|
|
458
524
|
// text line can't carry (`[image]`), so the dump says what it leaves out.
|
|
@@ -462,7 +528,7 @@ export class TuneOutCoordinator {
|
|
|
462
528
|
const historyTool = this.hooks.historyTool ? this.hooks.historyTool() : 'fetch_history';
|
|
463
529
|
// The channel as the resident and the subconscious are shown it.
|
|
464
530
|
const name = this.shown(serverId, channelId);
|
|
465
|
-
const lifecycle = `[${NOTICE_TAG.tuneOut}: ${name} cancelled — ${reason}, ${
|
|
531
|
+
const lifecycle = `[${NOTICE_TAG.tuneOut}: ${name} cancelled — ${reason}, ${ending.wakeCount} wake${ending.wakeCount === 1 ? '' : 's'}]`;
|
|
466
532
|
const render = (lines, truncated) => {
|
|
467
533
|
const attrs = [
|
|
468
534
|
`channel=${this.quotedShown(serverId, channelId)}`,
|
|
@@ -533,10 +599,26 @@ export class TuneOutCoordinator {
|
|
|
533
599
|
...heldLeftOut,
|
|
534
600
|
...held.slice(heldLeftOut.length).filter((m) => m.content.some((b) => b.type !== 'text') || (newestCut && m === shown[0])),
|
|
535
601
|
];
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
602
|
+
const records = [...earlier];
|
|
603
|
+
if (earlier.length === 0 || backlog.length > 0) {
|
|
604
|
+
const record = { ...keysOf(backlog), total: backlog.length, truncated, report: keysOf(heldForReport) };
|
|
605
|
+
this.hooks.addMessage('user', [{ type: 'text', text: render(lines, truncated) }], { system: true, kind: 'tune-out-cancel', channelId, epochId, released: record });
|
|
606
|
+
records.push(record);
|
|
607
|
+
}
|
|
608
|
+
if (this.hooks.commitDeliveries?.() === false) {
|
|
609
|
+
throw new Error(`the backlog of ${this.quotedShown(serverId, channelId)} could not be made durable, so its tune-out stays in force`);
|
|
610
|
+
}
|
|
611
|
+
this.channelRegistry.cancelTuneOut(serverId, channelId, nextState, source);
|
|
612
|
+
this.clearTimers(serverId, channelId);
|
|
613
|
+
this.divertedSinceInvocation.delete(this.key(serverId, channelId));
|
|
614
|
+
const reportedEarlier = keyedBy(earlier.map((r) => r.report));
|
|
615
|
+
const release = {
|
|
616
|
+
truncated: records.reduce((n, r) => n + r.truncated, 0),
|
|
617
|
+
heldForReport: [
|
|
618
|
+
...[...storedRows, ...queuedRows].filter((m) => ofEpoch(m) && reportedEarlier(m)),
|
|
619
|
+
...(records.length > earlier.length ? heldForReport : []),
|
|
620
|
+
],
|
|
621
|
+
};
|
|
540
622
|
// The subconscious's own words, under its own name, verbatim.
|
|
541
623
|
const subName = this.hooks.subconsciousName();
|
|
542
624
|
if (subconsciousNote && subName) {
|
|
@@ -564,17 +646,15 @@ export class TuneOutCoordinator {
|
|
|
564
646
|
// and when it wrote no note and the dump left any message out, since
|
|
565
647
|
// nothing else then covers it. Otherwise its note speaks for what it saw.
|
|
566
648
|
const selfCancel = source === 'subconscious';
|
|
567
|
-
if (subName && (!selfCancel || heldForReport.length > 0 || (!subconsciousNote && truncated > 0))) {
|
|
568
|
-
const truncNote =
|
|
569
|
-
|
|
570
|
-
: ''
|
|
571
|
-
const heldNote = heldForReport.length > 0
|
|
572
|
-
? ` Still held at the cancel, so your view may not have them yet: the ${heldForReport.length} message${heldForReport.length === 1 ? '' : 's'} after this one.`
|
|
649
|
+
if (subName && (!selfCancel || release.heldForReport.length > 0 || (!subconsciousNote && release.truncated > 0))) {
|
|
650
|
+
const truncNote = truncationNote(records);
|
|
651
|
+
const heldNote = release.heldForReport.length > 0
|
|
652
|
+
? ` Still held at the cancel, so your view may not have them yet: the ${release.heldForReport.length} message${release.heldForReport.length === 1 ? '' : 's'} after this one.`
|
|
573
653
|
: '';
|
|
574
654
|
this.hooks.addMessage('user', [{ type: 'text', text: `[${NOTICE_TAG.tuneOutCancelled}: ${this.shown(serverId, channelId)} — ${reason}.${truncNote}${heldNote}]` }], { system: true, kind: 'tune-out-cancel-report', channelId }, subName);
|
|
575
655
|
// Each held message is a row of its own, so a per-message cap cuts each
|
|
576
656
|
// one on its own, its author's label included, not the batch's newest.
|
|
577
|
-
for (const m of heldForReport) {
|
|
657
|
+
for (const m of release.heldForReport) {
|
|
578
658
|
this.hooks.addMessage('user', [{ type: 'text', text: `${authorOf(m)}:` }, ...m.content], { system: true, kind: 'tune-out-cancel-held', channelId }, subName);
|
|
579
659
|
}
|
|
580
660
|
this.hooks.requestInference(subName, `tune-out cancelled: ${name}`, 'tune-out');
|
|
@@ -592,9 +672,7 @@ export class TuneOutCoordinator {
|
|
|
592
672
|
const rest = this.unhandedBacklog(this.handOffKey(epochId, serverId, channelId), stored, queued);
|
|
593
673
|
const unread = rest.backlog.length + rest.queued.length;
|
|
594
674
|
if (!selfCancel || unread > 0) {
|
|
595
|
-
const truncNote =
|
|
596
|
-
? ` ${truncated} of ${backlog.length} diverted messages, the oldest, weren't delivered raw to the resident.`
|
|
597
|
-
: '';
|
|
675
|
+
const truncNote = truncationNote(records);
|
|
598
676
|
const lateNote = selfCancel
|
|
599
677
|
? ` ${unread === 1 ? 'This message' : `These ${unread} messages`} arrived after the reader that cancelled was handed its own.`
|
|
600
678
|
: '';
|
|
@@ -607,7 +685,7 @@ export class TuneOutCoordinator {
|
|
|
607
685
|
// there that its reader hasn't looked at are counted again, as the
|
|
608
686
|
// channel's own tune-out's count stood in for theirs.
|
|
609
687
|
this.rebuildTally(serverId, channelId);
|
|
610
|
-
this.hooks.emitTrace({ type: 'tune-out:cancelled', serverId, channelId, epochId, reason, wakeCount:
|
|
688
|
+
this.hooks.emitTrace({ type: 'tune-out:cancelled', serverId, channelId, epochId, reason, wakeCount: ending.wakeCount });
|
|
611
689
|
return { ok: true };
|
|
612
690
|
}
|
|
613
691
|
// ==========================================================================
|
|
@@ -814,20 +892,20 @@ export class TuneOutCoordinator {
|
|
|
814
892
|
}
|
|
815
893
|
return counts;
|
|
816
894
|
}
|
|
817
|
-
/**
|
|
818
|
-
* End the all-except hold: durable flip, one release notice with a
|
|
819
|
-
* per-channel backlog (each capped), the reader's note if any, one wake
|
|
820
|
-
* for the resident, and the reader's own look at what it had not seen.
|
|
821
|
-
*/
|
|
822
895
|
cancelAllExcept(source, reason, note, noteAuthor) {
|
|
823
896
|
const scope = this.getAllExcept();
|
|
824
897
|
if (!scope)
|
|
825
898
|
return { ok: false, error: 'no all-except hold is active' };
|
|
826
899
|
const { epochId, except, backlogCap } = scope.params;
|
|
827
900
|
const ofEpoch = (m) => m.metadata?.tuneOut?.epochId === epochId;
|
|
828
|
-
const
|
|
901
|
+
const storedRows = this.hooks.getStoredMessages();
|
|
902
|
+
const queuedRows = this.hooks.getDeferredMessages?.() ?? [];
|
|
903
|
+
const all = [...storedRows.filter(ofEpoch), ...queuedRows.filter(ofEpoch)];
|
|
904
|
+
const earlier = releaseRecordsOf([...storedRows, ...queuedRows], epochId);
|
|
905
|
+
const releasedEarlier = keyedBy(earlier);
|
|
906
|
+
const fresh = all.filter((m) => !releasedEarlier(m));
|
|
829
907
|
const byChannel = new Map();
|
|
830
|
-
for (const m of
|
|
908
|
+
for (const m of fresh) {
|
|
831
909
|
const meta = m.metadata;
|
|
832
910
|
const serverId = meta?.serverId ?? '';
|
|
833
911
|
const channelId = meta?.channelId ?? '(unknown channel)';
|
|
@@ -837,12 +915,14 @@ export class TuneOutCoordinator {
|
|
|
837
915
|
byChannel.set(k, entry);
|
|
838
916
|
}
|
|
839
917
|
const historyTool = this.hooks.historyTool?.() ?? null;
|
|
918
|
+
let truncatedInAll = 0;
|
|
840
919
|
const dumps = [...byChannel.values()].map(({ serverId, channelId, backlog }) => {
|
|
841
920
|
// Floored here too: a hold stored before entry floored its cap can hold
|
|
842
921
|
// a fraction.
|
|
843
922
|
const cap = Math.floor(backlogCap);
|
|
844
923
|
const shown = cap > 0 ? backlog.slice(-cap) : [];
|
|
845
924
|
const truncated = backlog.length - shown.length;
|
|
925
|
+
truncatedInAll += truncated;
|
|
846
926
|
const attrs = [
|
|
847
927
|
`channel=${quotedValue(this.shown(serverId, channelId))}`,
|
|
848
928
|
`messages=${backlog.length}`,
|
|
@@ -852,18 +932,15 @@ export class TuneOutCoordinator {
|
|
|
852
932
|
});
|
|
853
933
|
const summary = byChannel.size === 0
|
|
854
934
|
? 'Nothing was held.'
|
|
855
|
-
: `${
|
|
856
|
-
if (
|
|
857
|
-
|
|
858
|
-
this.hooks.
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
// this process records only the hold's end; a restart between the two
|
|
864
|
-
// delivers the release again, where the other order lost it outright.
|
|
935
|
+
: `${fresh.length} message${fresh.length === 1 ? '' : 's'} held in ${byChannel.size} channel${byChannel.size === 1 ? '' : 's'}.`;
|
|
936
|
+
if (earlier.length === 0 || fresh.length > 0) {
|
|
937
|
+
const record = { ...keysOf(fresh), total: fresh.length, truncated: truncatedInAll, report: { ids: [], writeIds: [] } };
|
|
938
|
+
this.hooks.addMessage('user', [{ type: 'text', text: `[${NOTICE_TAG.tuneOut}: all channels except ${this.shown(except.serverId, except.channelId)} released — ${reason}, ${scope.wakeCount} wake${scope.wakeCount === 1 ? '' : 's'}]\n\n${summary}${dumps.length > 0 ? '\n\n' + dumps.join('\n\n') : ''}` }], { system: true, kind: 'tune-out-cancel', scope: 'all-except', channelId: except.channelId, epochId, released: record });
|
|
939
|
+
}
|
|
940
|
+
if (this.hooks.commitDeliveries?.() === false) {
|
|
941
|
+
throw new Error('the all-except release could not be made durable, so the hold stays in force');
|
|
942
|
+
}
|
|
865
943
|
this.writeScope(null);
|
|
866
|
-
this.scopeReleased = undefined;
|
|
867
944
|
this.clearScopeTimers();
|
|
868
945
|
for (const [key, pending] of this.pendingWakes) {
|
|
869
946
|
const [serverId, channelId] = key.split('\0');
|
|
@@ -1090,6 +1167,34 @@ export class TuneOutCoordinator {
|
|
|
1090
1167
|
}, WAKE_COALESCE_MS);
|
|
1091
1168
|
this.pendingWakes.set(key, { count: 1, timer });
|
|
1092
1169
|
}
|
|
1170
|
+
cancelPastBudget(serverId, channelId, maxWakes, from = 'wake') {
|
|
1171
|
+
const key = this.key(serverId, channelId);
|
|
1172
|
+
const existing = this.expiryTimers.get(key);
|
|
1173
|
+
if (existing)
|
|
1174
|
+
clearTimeout(existing);
|
|
1175
|
+
const end = (first) => {
|
|
1176
|
+
try {
|
|
1177
|
+
this.expiryTimers.set(key, setTimeout(() => end(false), EXPIRY_RETRY_MS));
|
|
1178
|
+
if (!this.cancel(serverId, channelId, 'tune-out-max-wakes', `wake budget exhausted (${maxWakes})`).ok) {
|
|
1179
|
+
this.clearTimers(serverId, channelId);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
catch (err) {
|
|
1183
|
+
if (first && from === 'start') {
|
|
1184
|
+
if (this.channelRegistry.getTuneOutState(serverId, channelId)) {
|
|
1185
|
+
console.error(`[tune-out] ${this.shown(serverId, channelId)} was past its wake budget at start, and its cancel failed, so it stays tuned out and its cancel is tried again in ${EXPIRY_RETRY_MS / 1000} s:`, err);
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
console.error(`[tune-out] ${this.shown(serverId, channelId)} was past its wake budget at start and its cancel ended it, but a step after the end failed:`, err);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
1192
|
+
this.timerFailed('wake', serverId, channelId, err);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
end(true);
|
|
1197
|
+
}
|
|
1093
1198
|
/** One coalesced wake: counted durably against the epoch that holds the channel. */
|
|
1094
1199
|
fireWake(serverId, channelId, count) {
|
|
1095
1200
|
if (this.channelRegistry.getTuneOutState(serverId, channelId)) {
|
|
@@ -1098,7 +1203,7 @@ export class TuneOutCoordinator {
|
|
|
1098
1203
|
if (!counted)
|
|
1099
1204
|
return; // cancelled inside the window
|
|
1100
1205
|
if (counted.wakeCount > counted.params.maxWakes) {
|
|
1101
|
-
this.
|
|
1206
|
+
this.cancelPastBudget(serverId, channelId, counted.params.maxWakes);
|
|
1102
1207
|
return;
|
|
1103
1208
|
}
|
|
1104
1209
|
this.invokeSubconscious(serverId, channelId, 'wake', count);
|