@jjh32456/dsh-longlive 1.6.6 → 1.6.8
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/CHANGELOG.md +40 -4
- package/QUICKSTART.md +10 -10
- package/README.en.md +4 -3
- package/README.md +10 -4
- package/SPEC.md +2 -2
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CONFIG.md +33 -6
- package/docs/DEV.md +3 -3
- package/docs/FAQ.md +4 -4
- package/docs/FEATURES.md +36 -5
- package/docs/OPERATIONS.md +1 -1
- package/docs/QUICKSTART.md +3 -3
- package/docs/README.en.md +3 -2
- package/docs/TROUBLESHOOTING.md +3 -3
- package/package.json +13 -8
- package/src/activity/index.js +649 -0
- package/src/activity/model.js +341 -0
- package/src/activity/store.js +625 -0
- package/src/compaction/engine-core.js +144 -97
- package/src/config.js +29 -2
- package/src/features.js +26 -0
- package/src/heartbeat.js +156 -23
- package/src/identity/handover.js +52 -1
- package/src/live.js +22 -2
- package/src/recall/privacy.mjs +105 -16
- package/src/router/service/board.mjs +95 -15
- package/src/router/service/hint.mjs +14 -0
- package/src/router/service/service.mjs +27 -11
- package/test/activity-realtime.test.mjs +959 -0
- package/test/activity-stream.test.mjs +512 -0
- package/test/autobind.test.mjs +25 -11
- package/test/config-schema.test.mjs +6 -0
- package/test/feature-flags.test.mjs +17 -0
- package/test/fixtures/activity-append-child.mjs +29 -0
- package/test/hot-truncate.test.mjs +113 -0
- package/test/live.test.mjs +6 -3
- package/test/packaging.test.mjs +7 -5
- package/test/router-background/router.test.mjs +44 -12
- package/test/run-rc.mjs +16 -0
- package/test/safety-defaults.test.mjs +5 -3
- package/test/state-truth.test.mjs +937 -0
- package/test/version-consistency.test.mjs +286 -0
|
@@ -342,21 +342,31 @@ function roughPointerTokens(text) {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
/**
|
|
345
|
-
* Select the oldest whole
|
|
346
|
-
*
|
|
345
|
+
* Select the oldest whole closed turns that sit outside the retention window.
|
|
346
|
+
* Returns null when no safe cut exists; every such refusal reports a structured
|
|
347
|
+
* reason through `helpers.onNoPlan` so a silently disabled hot truncate is
|
|
348
|
+
* observable (the engine logs it once per reason).
|
|
347
349
|
*
|
|
348
|
-
* The
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
* same cut, which keeps exactly one pointer.
|
|
350
|
+
* The leading run of `system/message` / `developer/message` nodes (the real
|
|
351
|
+
* system prompt at host-protected position 0) is never moved. A `system/message`
|
|
352
|
+
* re-injected by a host restart further inside the surface is an archived copy
|
|
353
|
+
* of that same prompt (host-tolerated inside a replacement range). Checkpoint
|
|
354
|
+
* nodes at the head of the span are folded into the same cut, which keeps
|
|
355
|
+
* exactly one pointer.
|
|
355
356
|
*/
|
|
356
357
|
export function selectCutPlan(session, options, measurement, helpers = {}) {
|
|
357
358
|
const table = helpers.table ?? turnTable(session);
|
|
358
359
|
const groups = surfaceGroups(session, table, helpers.isCheckpointSource);
|
|
359
|
-
|
|
360
|
+
const onNoPlan = typeof helpers.onNoPlan === 'function' ? helpers.onNoPlan : null;
|
|
361
|
+
const noPlan = (reason, detail) => {
|
|
362
|
+
try {
|
|
363
|
+
onNoPlan?.({ reason, detail: detail === undefined ? null : detail });
|
|
364
|
+
} catch {
|
|
365
|
+
// Diagnostics never break a plan.
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
};
|
|
369
|
+
if (groups.length === 0) return noPlan('no-groups');
|
|
360
370
|
const tokens = new Map();
|
|
361
371
|
for (const node of measurement?.nodes ?? []) {
|
|
362
372
|
if (node && typeof node.seq === 'number' && Number.isFinite(node.tokens)) tokens.set(node.seq, node.tokens);
|
|
@@ -386,29 +396,37 @@ export function selectCutPlan(session, options, measurement, helpers = {}) {
|
|
|
386
396
|
break;
|
|
387
397
|
}
|
|
388
398
|
}
|
|
389
|
-
if (boundary <= 0) return
|
|
399
|
+
if (boundary <= 0) return noPlan('not-enough-turns', { groups: groups.length });
|
|
390
400
|
|
|
391
401
|
const surfaceNodes = session?.surface?.nodes ?? [];
|
|
392
|
-
if (surfaceNodes.length === 0) return
|
|
402
|
+
if (surfaceNodes.length === 0) return noPlan('no-surface');
|
|
393
403
|
|
|
394
|
-
// 2026-11-06 1.6.
|
|
404
|
+
// 2026-11-06 1.6.7 hardening — no permanent barrier, no silent zero progress.
|
|
395
405
|
//
|
|
396
|
-
// 1.6.
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
406
|
+
// 1.6.6 removed the aborted-turn freeze but treated every group without a
|
|
407
|
+
// usable turn record as an immovable open group and stopped the span before
|
|
408
|
+
// it. Three surface shapes then made the planner return null forever even
|
|
409
|
+
// though whole closed turns older than the retention window existed:
|
|
410
|
+
// * a log truncated mid-turn leaves orphan messages at the surface head
|
|
411
|
+
// (no turn/start) — a turn===0 group at the head (red team E1);
|
|
412
|
+
// * the host appends a user/message between turns — a turn===0 group in
|
|
413
|
+
// the middle (red team E3);
|
|
414
|
+
// * a turn whose turn/end was lost by log truncation — turn N has no
|
|
415
|
+
// endSeq but later turns exist (red team E2).
|
|
405
416
|
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
417
|
+
// Rules now:
|
|
418
|
+
// * the whole LEADING run of system/message + developer/message nodes is
|
|
419
|
+
// never moved (head instructions; host-protected position 0);
|
|
420
|
+
// * a turn===0 group that carries a system/developer node is a head
|
|
421
|
+
// instruction zone and stays in place; a turn===0 group of plain messages
|
|
422
|
+
// is an orphan prefix and is archived with the cut (recoverable);
|
|
423
|
+
// * a group whose turn has no turn/end is the CURRENT turn only when it is
|
|
424
|
+
// the last started turn; an earlier turn without turn/end is abandoned
|
|
425
|
+
// (the log was truncated inside it) and is cuttable, archived through an
|
|
426
|
+
// inferred range up to the next turn/start;
|
|
427
|
+
// * the planner skips a blocked head barrier and cuts the next run of
|
|
428
|
+
// wholly-cuttable groups; if nothing is cuttable it reports the reason
|
|
429
|
+
// through onNoPlan instead of returning null silently.
|
|
412
430
|
const groupStart = new Array(groups.length);
|
|
413
431
|
const groupEnd = new Array(groups.length);
|
|
414
432
|
const groupOfPosition = new Array(surfaceNodes.length);
|
|
@@ -420,99 +438,118 @@ export function selectCutPlan(session, options, measurement, helpers = {}) {
|
|
|
420
438
|
groupEnd[index] = cursor - 1;
|
|
421
439
|
}
|
|
422
440
|
}
|
|
441
|
+
const typeOfSeq = (seq) => table.bySeq.get(seq)?.type;
|
|
442
|
+
const isInstructionSeq = (seq) => typeOfSeq(seq) === 'system/message' || typeOfSeq(seq) === 'developer/message';
|
|
443
|
+
const protectedSeqs = new Set();
|
|
444
|
+
{
|
|
445
|
+
let position = 0;
|
|
446
|
+
while (position < surfaceNodes.length && isInstructionSeq(surfaceNodes[position])) {
|
|
447
|
+
protectedSeqs.add(surfaceNodes[position]);
|
|
448
|
+
position += 1;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const lastStarted = table.turns.length > 0 ? table.turns[table.turns.length - 1] : null;
|
|
423
452
|
const turnInfoOf = (group) => (group.kind === 'turn' && group.turn !== 0 ? table.byNumber.get(group.turn) : undefined);
|
|
424
|
-
const
|
|
453
|
+
const isCurrentTurnGroup = (group) => {
|
|
454
|
+
if (group.kind !== 'turn' || group.turn === 0) return false;
|
|
455
|
+
const info = table.byNumber.get(group.turn);
|
|
456
|
+
if (info === undefined) return true;
|
|
457
|
+
return info.endSeq === null && lastStarted !== null && info.turn === lastStarted.turn;
|
|
458
|
+
};
|
|
459
|
+
const isCuttableGroup = (group) => {
|
|
460
|
+
if (group === undefined) return false;
|
|
461
|
+
if (group.kind === 'checkpoint') return true;
|
|
425
462
|
if (group.kind !== 'turn') return false;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
463
|
+
// A turn===0 group is either an orphan head zone (contains instructions) or
|
|
464
|
+
// a stray message block between turns; only the latter may be archived.
|
|
465
|
+
if (group.turn === 0) return !group.seqs.some((seq) => isInstructionSeq(seq));
|
|
466
|
+
const info = table.byNumber.get(group.turn);
|
|
467
|
+
if (info === undefined) return false;
|
|
468
|
+
return !isCurrentTurnGroup(group);
|
|
469
|
+
};
|
|
470
|
+
// An abandoned turn (no turn/end, but a later turn/start exists) never got a
|
|
471
|
+
// closing event; the archive range is inferred up to the next turn/start so
|
|
472
|
+
// every one of its original events stays recoverable.
|
|
473
|
+
const resolvedEndSeq = (group) => {
|
|
474
|
+
const info = table.byNumber.get(group.turn);
|
|
475
|
+
const lastSurfaceSeq = group.seqs[group.seqs.length - 1];
|
|
476
|
+
if (info === undefined || !Number.isInteger(info.startSeq)) return lastSurfaceSeq;
|
|
477
|
+
if (Number.isInteger(info.endSeq)) return info.endSeq;
|
|
478
|
+
const next = table.turns.find((entry) => Number.isInteger(entry.startSeq) && entry.startSeq > info.startSeq);
|
|
479
|
+
if (next === undefined) return lastSurfaceSeq;
|
|
480
|
+
return Math.max(info.startSeq, next.startSeq - 1);
|
|
429
481
|
};
|
|
430
482
|
|
|
431
|
-
// The
|
|
432
|
-
//
|
|
433
|
-
//
|
|
483
|
+
// The span starts after the protected head run and on a user/assistant node
|
|
484
|
+
// (the pointer replaces one); a system copy skipped at the head of a turn
|
|
485
|
+
// stays outside the span exactly like 1.6.6 — it is never moved out.
|
|
434
486
|
let startIndex = 0;
|
|
435
|
-
while (startIndex < surfaceNodes.length
|
|
436
|
-
&& table.bySeq.get(surfaceNodes[startIndex])?.type === 'system/message') {
|
|
437
|
-
startIndex += 1;
|
|
438
|
-
}
|
|
439
|
-
// The pointer replaces a user/assistant message, so the first cut node must
|
|
440
|
-
// be one. A leading developer node is kept in place instead of cut.
|
|
487
|
+
while (startIndex < surfaceNodes.length && protectedSeqs.has(surfaceNodes[startIndex])) startIndex += 1;
|
|
441
488
|
while (startIndex < surfaceNodes.length) {
|
|
442
|
-
const type =
|
|
489
|
+
const type = typeOfSeq(surfaceNodes[startIndex]);
|
|
443
490
|
if (type === 'user/message' || type === 'assistant/message') break;
|
|
444
491
|
startIndex += 1;
|
|
445
492
|
}
|
|
446
|
-
if (startIndex >= surfaceNodes.length) return
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
let
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const type = table.bySeq.get(surfaceNodes[startIndex])?.type;
|
|
462
|
-
if (type === 'user/message' || type === 'assistant/message') break;
|
|
463
|
-
startIndex += 1;
|
|
493
|
+
if (startIndex >= surfaceNodes.length) return noPlan('no-span-start');
|
|
494
|
+
const firstGroup = groupOfPosition[startIndex];
|
|
495
|
+
|
|
496
|
+
// Earliest run of consecutive cuttable groups that lies entirely before the
|
|
497
|
+
// retention boundary and holds at least one real closed turn. Runs made only
|
|
498
|
+
// of orphan messages/pointers are not worth a surface rewrite: skip them.
|
|
499
|
+
let chosenStart = -1;
|
|
500
|
+
let chosenEnd = -1;
|
|
501
|
+
for (let index = firstGroup; index < boundary; index += 1) {
|
|
502
|
+
if (!isCuttableGroup(groups[index])) continue;
|
|
503
|
+
let runEnd = index;
|
|
504
|
+
while (runEnd + 1 < boundary && isCuttableGroup(groups[runEnd + 1])) runEnd += 1;
|
|
505
|
+
let realTurns = 0;
|
|
506
|
+
for (let cursor = index; cursor <= runEnd; cursor += 1) {
|
|
507
|
+
if (groups[cursor].kind === 'turn' && groups[cursor].turn !== 0) realTurns += 1;
|
|
464
508
|
}
|
|
465
|
-
if (
|
|
466
|
-
|
|
509
|
+
if (realTurns > 0) { chosenStart = index; chosenEnd = runEnd; break; }
|
|
510
|
+
index = runEnd;
|
|
467
511
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
let endGroup = boundary - 1;
|
|
475
|
-
let blockedAt = -1;
|
|
476
|
-
for (let index = startGroup; index <= endGroup; index += 1) {
|
|
477
|
-
if (isOpenGroup(groups[index])) { blockedAt = index; break; }
|
|
512
|
+
if (chosenStart < 0) {
|
|
513
|
+
return noPlan('blocked-prefix', {
|
|
514
|
+
firstGroup,
|
|
515
|
+
boundary,
|
|
516
|
+
blocked: groups.slice(firstGroup, boundary).map((group) => `${group.kind}:${group.turn}:${group.completed}`),
|
|
517
|
+
});
|
|
478
518
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
519
|
+
let spanStart = Math.max(startIndex, groupStart[chosenStart]);
|
|
520
|
+
const endIndex = groupEnd[chosenEnd];
|
|
521
|
+
if (endIndex < spanStart) return noPlan('empty-span', { chosenStart, chosenEnd });
|
|
482
522
|
|
|
483
523
|
// A system/message node *inside* the span is a host-restart re-injection of
|
|
484
|
-
// the same prompt
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
// injected copy each) outside every future cut, so the context could never
|
|
488
|
-
// reach the retention window. The host tolerates later system nodes inside a
|
|
489
|
-
// replacement range, so an injected copy is archived together with the old
|
|
490
|
-
// prefix it belongs to. Nodes before the span start are never touched.
|
|
491
|
-
const shadowedSeqs = surfaceNodes.slice(startIndex, endIndex + 1);
|
|
524
|
+
// the same prompt; the host tolerates it inside a replacement range and it is
|
|
525
|
+
// archived together with the old prefix (approved 1.6.6 behaviour).
|
|
526
|
+
const shadowedSeqs = surfaceNodes.slice(spanStart, endIndex + 1);
|
|
492
527
|
const shadowedSet = new Set(shadowedSeqs);
|
|
493
528
|
const touchedGroups = groups.filter((group) => group.seqs.some((seq) => shadowedSet.has(seq)));
|
|
494
|
-
if (touchedGroups.some((group) =>
|
|
529
|
+
if (touchedGroups.some((group) => isCurrentTurnGroup(group))) return noPlan('current-turn-touched');
|
|
530
|
+
// No half turn: every touched turn group must be fully shadowed. The only
|
|
531
|
+
// allowed exception is a system/developer copy skipped at the head of the
|
|
532
|
+
// first group (it stays on the surface, never half-cut).
|
|
495
533
|
for (const group of touchedGroups) {
|
|
496
534
|
if (group.kind !== 'turn') continue;
|
|
497
535
|
const missing = group.seqs.filter((seq) => !shadowedSet.has(seq));
|
|
498
|
-
if (missing.some((seq) =>
|
|
536
|
+
if (missing.some((seq) => !isInstructionSeq(seq))) return noPlan('split-group', { turn: group.turn });
|
|
499
537
|
}
|
|
500
538
|
const cutTurnGroups = touchedGroups.filter((group) => {
|
|
501
539
|
if (group.kind !== 'turn' || group.turn === 0) return false;
|
|
502
|
-
|
|
503
|
-
return info !== undefined && info.endSeq !== null;
|
|
540
|
+
return !isCurrentTurnGroup(group);
|
|
504
541
|
});
|
|
505
|
-
if (cutTurnGroups.length === 0) return
|
|
542
|
+
if (cutTurnGroups.length === 0) return noPlan('no-turns', { chosenStart, chosenEnd });
|
|
506
543
|
|
|
507
|
-
// Archive every original event of the cut turns, plus any checkpoint node
|
|
508
|
-
// that positionally lived inside the replaced span
|
|
544
|
+
// Archive every original event of the cut turns, plus any checkpoint node and
|
|
545
|
+
// orphan (turn===0) message that positionally lived inside the replaced span.
|
|
509
546
|
const archivedSeqs = new Set();
|
|
510
547
|
const archiveEvents = [];
|
|
511
548
|
for (const group of cutTurnGroups) {
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
for (let seq = startSeq; seq <= endSeq; seq += 1) {
|
|
549
|
+
const info = table.byNumber.get(group.turn);
|
|
550
|
+
if (info === undefined || !Number.isInteger(info.startSeq)) continue;
|
|
551
|
+
const endSeq = resolvedEndSeq(group);
|
|
552
|
+
for (let seq = info.startSeq; seq <= endSeq; seq += 1) {
|
|
516
553
|
const event = table.bySeq.get(seq);
|
|
517
554
|
if (event === undefined || archivedSeqs.has(seq)) continue;
|
|
518
555
|
archivedSeqs.add(seq);
|
|
@@ -536,13 +573,13 @@ export function selectCutPlan(session, options, measurement, helpers = {}) {
|
|
|
536
573
|
firstSeq: seqs[0],
|
|
537
574
|
lastSeq: seqs[seqs.length - 1],
|
|
538
575
|
startSeq: info?.startSeq ?? seqs[0],
|
|
539
|
-
endSeq:
|
|
576
|
+
endSeq: resolvedEndSeq(group),
|
|
540
577
|
surfaceEvents: seqs.length,
|
|
541
578
|
tokens: seqs.reduce((total, seq) => total + prices.get(seq), 0),
|
|
542
579
|
};
|
|
543
580
|
});
|
|
544
581
|
const shadowedTokenCount = shadowedSeqs.reduce((total, seq) => total + prices.get(seq), 0);
|
|
545
|
-
if (shadowedTokenCount <= 0) return
|
|
582
|
+
if (shadowedTokenCount <= 0) return noPlan('zero-token');
|
|
546
583
|
|
|
547
584
|
const lastTouchedIndex = groups.indexOf(touchedGroups[touchedGroups.length - 1]);
|
|
548
585
|
const retainedGroups = groups.slice(lastTouchedIndex + 1);
|
|
@@ -569,7 +606,9 @@ export function selectCutPlan(session, options, measurement, helpers = {}) {
|
|
|
569
606
|
retainedTurns,
|
|
570
607
|
});
|
|
571
608
|
const pointerTokens = roughPointerTokens(previewText);
|
|
572
|
-
if (shadowedTokenCount < pointerTokens + options.minCutTokens)
|
|
609
|
+
if (shadowedTokenCount < pointerTokens + options.minCutTokens) {
|
|
610
|
+
return noPlan('pointer-too-large', { shadowedTokenCount, pointerTokens });
|
|
611
|
+
}
|
|
573
612
|
|
|
574
613
|
return {
|
|
575
614
|
start: shadowedSeqs[0],
|
|
@@ -895,6 +934,14 @@ export function createCutEngine(deps) {
|
|
|
895
934
|
return selectCutPlan(session, this.options, measurement, {
|
|
896
935
|
table,
|
|
897
936
|
isCheckpointSource: isCompactCheckpointSource,
|
|
937
|
+
// "cut, or say why not": a refusal that would otherwise be silent is
|
|
938
|
+
// logged once per reason (warnOnce) so a permanently disabled hot
|
|
939
|
+
// truncate is always observable in the host log.
|
|
940
|
+
onNoPlan: (info) => {
|
|
941
|
+
const reason = String(info?.reason ?? 'unknown');
|
|
942
|
+
const detail = info?.detail === undefined || info?.detail === null ? '' : ` ${JSON.stringify(info.detail)}`;
|
|
943
|
+
this.warnOnce(`no-plan:${reason}`, `hot-truncate: 未切(${reason})${detail}`);
|
|
944
|
+
},
|
|
898
945
|
});
|
|
899
946
|
}
|
|
900
947
|
|
package/src/config.js
CHANGED
|
@@ -54,7 +54,7 @@ const ZH = {
|
|
|
54
54
|
recoverOnStart: '启动时收尾或回滚未完成的可见截断事务(默认开启)。',
|
|
55
55
|
keepEvents: 'session_truncate 默认保留的事件数(默认 20000)。',
|
|
56
56
|
recentWriteWindowMs: '最近写入保护窗口;窗口内拒绝截断(默认 60000 毫秒)。',
|
|
57
|
-
heartbeatMs: '活跃会话心跳间隔(默认 15000
|
|
57
|
+
heartbeatMs: '活跃会话心跳间隔(默认 15000 毫秒)。0 只关定时心跳与工具调用触发的写;turn 开始/结束仍按状态机维护 state/turn。',
|
|
58
58
|
routerEnabled: '开启 Router 路由服务:判定工具调用并按策略记录/拦截(默认关闭)。',
|
|
59
59
|
routerRuntime: '路由运行时模式:off / shadow / on(默认 off;要记录判定须显式设 shadow 或 on)。',
|
|
60
60
|
routerProvider: 'Router 使用的 provider;留空则用 deepseek-official。',
|
|
@@ -107,6 +107,15 @@ const ZH = {
|
|
|
107
107
|
pollMs: '总线轮询间隔(默认 3000 毫秒;0 关闭轮询)。',
|
|
108
108
|
root: '总线根目录;留空则使用记忆根。',
|
|
109
109
|
},
|
|
110
|
+
sharedBrain: {
|
|
111
|
+
$description: '共享大脑活动流:把各会话「正在干什么」写进活动板,每轮只注入未读的新增条目。',
|
|
112
|
+
activity: {
|
|
113
|
+
$description: '活动流开关与注入预算(与 presenceBus 无关,失败静默)。',
|
|
114
|
+
enabled: '开启共享大脑活动流(默认关闭)。',
|
|
115
|
+
injectMaxChars: '每次注入的总字符上限,含固定头尾(默认 300,范围 60–2000)。',
|
|
116
|
+
maxItems: '每次最多注入的活动条数(默认 5,范围 1–10)。',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
110
119
|
autoMaintenance: {
|
|
111
120
|
$description: '每日维护:本地午夜调度 consolidate 报告与 forget 候选清单。',
|
|
112
121
|
enabled: '开启每日维护(默认关闭)。',
|
|
@@ -162,7 +171,7 @@ const EN = {
|
|
|
162
171
|
recoverOnStart: 'Finish or roll back an interrupted visible-truncation transaction at startup (on by default).',
|
|
163
172
|
keepEvents: 'Default events kept by session_truncate (20000).',
|
|
164
173
|
recentWriteWindowMs: 'Recent-write guard window; truncation is refused inside it (60000 ms).',
|
|
165
|
-
heartbeatMs: 'Live-session heartbeat interval (15000 ms
|
|
174
|
+
heartbeatMs: 'Live-session heartbeat interval (15000 ms). 0 disables only the timer heartbeat and tool-touch writes; turn start/end still maintain state/turn.',
|
|
166
175
|
routerEnabled: 'Enable the Router service: evaluate tool calls and record/deny them by policy (off by default).',
|
|
167
176
|
routerRuntime: 'Router runtime mode: off / shadow / on (default off; set shadow or on to record evaluations).',
|
|
168
177
|
routerProvider: 'Provider used by Router; empty means deepseek-official.',
|
|
@@ -215,6 +224,15 @@ const EN = {
|
|
|
215
224
|
pollMs: 'Bus polling interval (default 3000 ms; 0 disables polling).',
|
|
216
225
|
root: 'Bus root directory; empty means the memory root.',
|
|
217
226
|
},
|
|
227
|
+
sharedBrain: {
|
|
228
|
+
$description: 'Shared-brain activity stream: one board row per turn, and only unread rows are injected.',
|
|
229
|
+
activity: {
|
|
230
|
+
$description: 'Activity-stream switches and injection budget (independent of presenceBus; failures are silent).',
|
|
231
|
+
enabled: 'Enable the shared-brain activity stream (off by default).',
|
|
232
|
+
injectMaxChars: 'Total characters per injection, header/footer included (default 300, range 60–2000).',
|
|
233
|
+
maxItems: 'Maximum activity rows per injection (default 5, range 1–10).',
|
|
234
|
+
},
|
|
235
|
+
},
|
|
218
236
|
autoMaintenance: {
|
|
219
237
|
$description: 'Daily maintenance: schedule consolidate reports and forget candidates at local midnight.',
|
|
220
238
|
enabled: 'Enable daily maintenance (off by default).',
|
|
@@ -292,6 +310,14 @@ function schemaOf() {
|
|
|
292
310
|
root: z.string().description(ZH.presenceBus.root),
|
|
293
311
|
}).collapse();
|
|
294
312
|
|
|
313
|
+
const sharedBrain = z.object({
|
|
314
|
+
activity: z.object({
|
|
315
|
+
enabled: z.boolean().default(false).description(ZH.sharedBrain.activity.enabled),
|
|
316
|
+
injectMaxChars: z.number().default(300).description(ZH.sharedBrain.activity.injectMaxChars),
|
|
317
|
+
maxItems: z.number().default(5).description(ZH.sharedBrain.activity.maxItems),
|
|
318
|
+
}).collapse(),
|
|
319
|
+
}).collapse();
|
|
320
|
+
|
|
295
321
|
const autoMaintenance = z.object({
|
|
296
322
|
enabled: z.boolean().default(false).description(ZH.autoMaintenance.enabled),
|
|
297
323
|
consolidate: z.boolean().default(true).description(ZH.autoMaintenance.consolidate),
|
|
@@ -377,6 +403,7 @@ function schemaOf() {
|
|
|
377
403
|
autoRecall,
|
|
378
404
|
identityCard,
|
|
379
405
|
presenceBus,
|
|
406
|
+
sharedBrain,
|
|
380
407
|
autoMaintenance,
|
|
381
408
|
routerFace,
|
|
382
409
|
autoTruncate,
|
package/src/features.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// identityCard.enabled -> agent/pre-step (open card, prepend:true)
|
|
10
10
|
// autoRecall.enabled -> agent/pre-step (recall, prepend:false) + session/event
|
|
11
11
|
// presenceBus.enabled -> session/event (drain this session's bus inbox)
|
|
12
|
+
// sharedBrain.activity.enabled -> session/event (write this turn's activity)
|
|
13
|
+
// + agent/pre-step (inject other sessions' new rows)
|
|
12
14
|
// autoMaintenance.enabled -> one unref'd timer to the next local midnight
|
|
13
15
|
// (consolidate report + forget dry-run pending list;
|
|
14
16
|
// no turn hook, so a failure cannot affect a turn)
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
import { apply as applyAutoRecall } from './recall/index.js';
|
|
41
43
|
import { createBus, normalizeSessionId, shouldAckOnTurnEnd } from './bus/bus.mjs';
|
|
42
44
|
import { createMaintenanceScheduler } from './memory/maintenance.js';
|
|
45
|
+
import { normalizeActivityConfig, registerActivityStream } from './activity/index.js';
|
|
43
46
|
import {
|
|
44
47
|
claimFallbackInjection,
|
|
45
48
|
markReceiptFallbackInjected,
|
|
@@ -52,6 +55,9 @@ export const FEATURE_DEFAULTS = Object.freeze({
|
|
|
52
55
|
autoRecall: Object.freeze({ enabled: false, topK: 3, budgetTokens: 2000, everyNTurns: 6 }),
|
|
53
56
|
identityCard: Object.freeze({ enabled: false, path: '<memoryRoot>/STATE.md', maxChars: 800 }),
|
|
54
57
|
presenceBus: Object.freeze({ enabled: false, pollMs: 3000 }),
|
|
58
|
+
sharedBrain: Object.freeze({
|
|
59
|
+
activity: Object.freeze({ enabled: false, injectMaxChars: 300, maxItems: 5 }),
|
|
60
|
+
}),
|
|
55
61
|
autoMaintenance: Object.freeze({
|
|
56
62
|
enabled: false,
|
|
57
63
|
consolidate: true,
|
|
@@ -81,6 +87,8 @@ export function normalizeFeatureConfig(config = {}) {
|
|
|
81
87
|
const auto = value.autoRecall && typeof value.autoRecall === 'object' ? value.autoRecall : {};
|
|
82
88
|
const identity = value.identityCard && typeof value.identityCard === 'object' ? value.identityCard : {};
|
|
83
89
|
const bus = value.presenceBus && typeof value.presenceBus === 'object' ? value.presenceBus : {};
|
|
90
|
+
const sharedBrain = value.sharedBrain && typeof value.sharedBrain === 'object' ? value.sharedBrain : {};
|
|
91
|
+
const activity = sharedBrain.activity && typeof sharedBrain.activity === 'object' ? sharedBrain.activity : {};
|
|
84
92
|
const maintenance = value.autoMaintenance && typeof value.autoMaintenance === 'object'
|
|
85
93
|
? value.autoMaintenance
|
|
86
94
|
: {};
|
|
@@ -105,6 +113,9 @@ export function normalizeFeatureConfig(config = {}) {
|
|
|
105
113
|
pollMs: clampInt(bus.pollMs, FEATURE_DEFAULTS.presenceBus.pollMs, 0, 3600000),
|
|
106
114
|
root: text(bus.root),
|
|
107
115
|
},
|
|
116
|
+
sharedBrain: {
|
|
117
|
+
activity: normalizeActivityConfig(activity),
|
|
118
|
+
},
|
|
108
119
|
autoMaintenance: {
|
|
109
120
|
enabled: bool(maintenance.enabled),
|
|
110
121
|
consolidate: maintenance.consolidate !== false,
|
|
@@ -566,6 +577,21 @@ export function registerFeatures(ctx, config = {}, options = {}) {
|
|
|
566
577
|
enabled.push('presenceBus');
|
|
567
578
|
}
|
|
568
579
|
|
|
580
|
+
if (cfg.sharedBrain.activity.enabled) {
|
|
581
|
+
if (!memoryRoot) {
|
|
582
|
+
warn('[longlive] sharedBrain.activity 已开启但没有可解析的 memoryRoot — 已跳过');
|
|
583
|
+
} else {
|
|
584
|
+
const stream = registerActivityStream(ctx, cfg.sharedBrain.activity, {
|
|
585
|
+
memoryRoot,
|
|
586
|
+
// Test seam only; the host config never carries these.
|
|
587
|
+
now: config.now,
|
|
588
|
+
privacyScan: typeof config.activityPrivacyScan === 'function' ? config.activityPrivacyScan : undefined,
|
|
589
|
+
logger: warn,
|
|
590
|
+
});
|
|
591
|
+
if (stream.enabled) enabled.push('sharedBrain.activity');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
569
595
|
if (cfg.autoMaintenance.enabled) {
|
|
570
596
|
if (!memoryRoot) {
|
|
571
597
|
warn('[longlive] autoMaintenance 已开启但没有可解析的 memoryRoot — 已跳过');
|