@jjh32456/dsh-longlive 1.6.7 → 1.8.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.
Files changed (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -0,0 +1,760 @@
1
+ // activity/index.js — shared-brain activity stream wiring (1.6.1, event-level).
2
+ //
3
+ // Producer: `session/event` writes into <memoryRoot>/.longlive/board/activity.jsonl.
4
+ // - `tool/call` (and `tool/result` with isError) refresh this turn's 「最近动作」
5
+ // row immediately, as 「工具:目标」 (e.g. 「pwsh:libtv 锚图 S08-S10」), so a
6
+ // sibling session sees a >10-minute turn while it is still running. Same-key
7
+ // refreshes are throttled to ≤1 write/10s; a merge bumps the row's `seq`
8
+ // (touchSeq) so the injection cursor below actually sees the refresh.
9
+ // - `turn/end` writes the local-rule conclusion row (user gist · assistant
10
+ // conclusion · latest tool action, ≤140 chars) as before.
11
+ // No model call, no session-log scan, no tool invocation.
12
+ //
13
+ // Consumer: `agent/pre-step` (step 1) → inject only rows with seq greater than
14
+ // this session's lastSeenSeq, other sessions only, at most `maxItems` rows and
15
+ // at most `injectMaxChars` characters (header/footer included). No rows means
16
+ // no write — zero token cost. Because every event-level refresh bumps seq, the
17
+ // cursor never gets stuck behind a long turn's first row.
18
+ //
19
+ // Failure policy: every path is wrapped; a broken board, a missing lock, a
20
+ // privacy verdict or a host without @deepseek-ai/dsh-llm just skips this
21
+ // feature. A turn is never blocked by the activity stream.
22
+
23
+ import path from 'node:path';
24
+
25
+ import { isUserAuthoredMessage } from '../truncate-visible.js';
26
+ import {
27
+ assertInjectionSource,
28
+ injectionSource,
29
+ insertionIndex,
30
+ isHumanMessage,
31
+ textOfMessage,
32
+ } from '../identity/inject-position.js';
33
+ import { privacyScan as defaultPrivacyScan } from '../recall/privacy.mjs';
34
+ import { startPrestepPhase, notePrestepDegrade } from '../metrics/prestep-latency.mjs';
35
+ import {
36
+ ACTIVITY_HINT_MARK,
37
+ ACTIVITY_REFRESH_THROTTLE_MS,
38
+ buildActivityKey,
39
+ buildActivityText,
40
+ buildToolActivityText,
41
+ charLength,
42
+ classifyKind,
43
+ clip,
44
+ oneLine,
45
+ renderActivityHint,
46
+ selectActivities,
47
+ shortSessionId,
48
+ } from './model.js';
49
+ import {
50
+ THINKING_HINT_MARK,
51
+ extractThinkingText,
52
+ isThinkingPayloadSafe,
53
+ renderThinkingHint,
54
+ selectThinkingActivities,
55
+ summarizeThinking,
56
+ } from './thinking.js';
57
+ import {
58
+ ACTIVITY_SESSION_RE,
59
+ activityBoardFile,
60
+ createActivityStore,
61
+ readJsonFile,
62
+ writeJsonAtomic,
63
+ } from './store.js';
64
+
65
+ export const ACTIVITY_STATE_VERSION = 1;
66
+ export const ACTIVITY_STATE_DIR = path.join('.longlive', 'bridge');
67
+
68
+ export const ACTIVITY_DEFAULTS = Object.freeze({
69
+ // 1.8.0 product default: real-time shared memory is ON by default; users opt
70
+ // out with an explicit `enabled: false` (never by omission).
71
+ enabled: true,
72
+ injectMaxChars: 300,
73
+ maxItems: 5,
74
+ });
75
+
76
+ function clampInt(value, fallback, min, max) {
77
+ const number = Number(value);
78
+ if (!Number.isFinite(number)) return fallback;
79
+ return Math.min(max, Math.max(min, Math.floor(number)));
80
+ }
81
+
82
+ /** Pure normalizer; the config schema uses the same defaults. */
83
+ export function normalizeActivityConfig(input = {}) {
84
+ const raw = input !== null && typeof input === 'object' ? input : {};
85
+ return {
86
+ // Default ON (1.8.0): only a literal false opts out.
87
+ enabled: raw.enabled !== false,
88
+ injectMaxChars: clampInt(raw.injectMaxChars, ACTIVITY_DEFAULTS.injectMaxChars, 60, 2000),
89
+ maxItems: clampInt(raw.maxItems, ACTIVITY_DEFAULTS.maxItems, 1, 10),
90
+ createUserMessage: typeof raw.createUserMessage === 'function' ? raw.createUserMessage : undefined,
91
+ };
92
+ }
93
+
94
+ export function activityStateFile(memoryRoot, sessionId) {
95
+ return path.join(String(memoryRoot ?? ''), ACTIVITY_STATE_DIR, `${sessionId}.activity.json`);
96
+ }
97
+
98
+ export function emptyActivityState(sessionId) {
99
+ return {
100
+ v: ACTIVITY_STATE_VERSION,
101
+ sessionId,
102
+ lastSeenSeq: 0,
103
+ lastInjectedSeq: 0,
104
+ lastInjectedAt: null,
105
+ injectedTurns: 0,
106
+ resets: 0,
107
+ lastResetAt: null,
108
+ updatedAt: null,
109
+ };
110
+ }
111
+
112
+ export function readActivityState(memoryRoot, sessionId) {
113
+ const read = readJsonFile(activityStateFile(memoryRoot, sessionId));
114
+ const value = read.ok ? read.value : null;
115
+ if (!value || value.v !== ACTIVITY_STATE_VERSION || value.sessionId !== sessionId) {
116
+ return emptyActivityState(sessionId);
117
+ }
118
+ return {
119
+ ...emptyActivityState(sessionId),
120
+ ...value,
121
+ lastSeenSeq: Number.isInteger(value.lastSeenSeq) && value.lastSeenSeq >= 0 ? value.lastSeenSeq : 0,
122
+ lastInjectedSeq: Number.isInteger(value.lastInjectedSeq) && value.lastInjectedSeq >= 0 ? value.lastInjectedSeq : 0,
123
+ injectedTurns: Number.isInteger(value.injectedTurns) && value.injectedTurns >= 0 ? value.injectedTurns : 0,
124
+ resets: Number.isInteger(value.resets) && value.resets >= 0 ? value.resets : 0,
125
+ };
126
+ }
127
+
128
+ function textBlocks(content) {
129
+ if (!Array.isArray(content)) return '';
130
+ const parts = [];
131
+ for (const block of content) {
132
+ if (!block || block.type !== 'text' || typeof block.text !== 'string') continue;
133
+ const text = block.text.trim();
134
+ if (text !== '') parts.push(text);
135
+ }
136
+ return parts.join('\n');
137
+ }
138
+
139
+ function messageTextOf(event) {
140
+ const data = event?.data ?? {};
141
+ const message = data.message && typeof data.message === 'object' ? data.message : data;
142
+ return textBlocks(message?.content);
143
+ }
144
+
145
+ function sessionIdOfMessage(session, event) {
146
+ const raw = session?.id
147
+ ?? session?.header?.id
148
+ ?? event?.sessionId
149
+ ?? '';
150
+ if (typeof raw !== 'string') return '';
151
+ const id = raw.trim().toLowerCase();
152
+ return ACTIVITY_SESSION_RE.test(id) ? id : '';
153
+ }
154
+
155
+ function toolSummaryOf(data) {
156
+ const name = oneLine(data?.name ?? 'tool');
157
+ const args = data?.arguments;
158
+ let snippet = '';
159
+ if (typeof args === 'string' && args.trim() !== '') {
160
+ let parsed = null;
161
+ try {
162
+ parsed = JSON.parse(args);
163
+ } catch {
164
+ parsed = null;
165
+ }
166
+ if (parsed && typeof parsed === 'object') {
167
+ for (const key of ['command', 'cmd', 'path', 'file_path', 'query', 'pattern', 'glob', 'description', 'prompt', 'url']) {
168
+ const value = parsed[key];
169
+ if (typeof value === 'string' && value.trim() !== '') {
170
+ snippet = value;
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ if (snippet === '') snippet = args;
176
+ }
177
+ return { name, summary: clip(oneLine(snippet), 44) };
178
+ }
179
+
180
+ function headerOf(payload) {
181
+ const agent = payload?.agent;
182
+ return agent?.session?.header ?? agent?.header ?? payload?.session?.header ?? {};
183
+ }
184
+
185
+ function sessionOf(payload) {
186
+ const agent = payload?.agent;
187
+ return agent?.session ?? payload?.session ?? null;
188
+ }
189
+
190
+ function preStepSessionId(payload) {
191
+ const header = headerOf(payload);
192
+ const agent = payload?.agent;
193
+ const raw = header.id ?? agent?.sessionId ?? sessionOf(payload)?.id ?? payload?.sessionId;
194
+ if (typeof raw !== 'string') return '';
195
+ const id = raw.trim().toLowerCase();
196
+ return ACTIVITY_SESSION_RE.test(id) ? id : '';
197
+ }
198
+
199
+ function normalizeOrigin(value) {
200
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
201
+ }
202
+
203
+ function maxSeqOf(entries) {
204
+ let max = 0;
205
+ for (const entry of Array.isArray(entries) ? entries : []) {
206
+ if (Number.isInteger(entry?.seq) && entry.seq > max) max = entry.seq;
207
+ }
208
+ return max;
209
+ }
210
+
211
+ async function resolveCreateUserMessage(explicit) {
212
+ if (typeof explicit === 'function') return explicit;
213
+ try {
214
+ const mod = await import('@deepseek-ai/dsh-llm');
215
+ return typeof mod?.createUserMessage === 'function' ? mod.createUserMessage : null;
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Create the stream object. `enabled` + `memoryRoot` are the only switches;
223
+ * everything else is a budget. Tests drive `onSessionEvent` / `maybeInject`
224
+ * directly, which is the same path the hooks use.
225
+ */
226
+ export function createActivityStream(options = {}) {
227
+ const cfg = normalizeActivityConfig(options);
228
+ // 1.8.0: thinking summaries are an independent, default-off injection switch.
229
+ // The write side also requires it, so off is byte-for-byte the 1.7.4 behaviour.
230
+ const thinkingInjection = options.thinkingInjection === true;
231
+ const memoryRoot = typeof options.memoryRoot === 'string' && options.memoryRoot.trim() !== ''
232
+ ? path.resolve(options.memoryRoot)
233
+ : null;
234
+ const now = typeof options.now === 'function' ? options.now : () => new Date();
235
+ const privacyScan = typeof options.privacyScan === 'function' ? options.privacyScan : defaultPrivacyScan;
236
+ const logger = typeof options.logger === 'function' ? options.logger : () => {};
237
+ const stats = {
238
+ turns: 0,
239
+ events: 0,
240
+ written: 0,
241
+ refreshed: 0,
242
+ merged: 0,
243
+ distilled: 0,
244
+ throttled: 0,
245
+ injected: 0,
246
+ skipped: 0,
247
+ privacy: 0,
248
+ resets: 0,
249
+ errors: 0,
250
+ thinkingWritten: 0,
251
+ thinkingInjected: 0,
252
+ };
253
+ const sessions = new Map();
254
+ // `${sessionId}|${key}` → { at: ms, kind } — 同 key 节流 ≤1 次刷新/10s.
255
+ const refreshes = new Map();
256
+ const disposers = [];
257
+ const store = memoryRoot ? createActivityStore({ memoryRoot, now, privacyScan }) : null;
258
+ const enabled = cfg.enabled === true && store !== null;
259
+
260
+ function stateFor(sessionId) {
261
+ let state = sessions.get(sessionId);
262
+ if (!state) {
263
+ state = {
264
+ sessionId,
265
+ turn: null,
266
+ userText: '',
267
+ assistantText: '',
268
+ tool: null,
269
+ toolCalls: 0,
270
+ thinkingTurn: null,
271
+ thinkingWritten: false,
272
+ };
273
+ sessions.set(sessionId, state);
274
+ }
275
+ return state;
276
+ }
277
+
278
+ function clearSessionRefreshes(sessionId) {
279
+ const prefix = `${sessionId}|`;
280
+ for (const key of refreshes.keys()) {
281
+ if (key.startsWith(prefix)) refreshes.delete(key);
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Event-level write: one refresh per tool call, as 「工具:目标」.
287
+ *
288
+ * Same-key refreshes are throttled to one per ACTIVITY_REFRESH_THROTTLE_MS —
289
+ * a 12-call burst must not append 12 near-identical rows. A tool error may
290
+ * skip the window once (kind escalation to blocked), after that it throttles
291
+ * like everything else. The store merge bumps `seq`, which is what lets the
292
+ * consumer's seq-only cursor pick the update up.
293
+ */
294
+ function writeToolActivity(state, { isError = false } = {}) {
295
+ const tool = state.tool;
296
+ if (!tool || typeof tool.name !== 'string' || tool.name.trim() === '') {
297
+ return { written: false, reason: 'no-tool' };
298
+ }
299
+ const failed = isError === true || tool.isError === true;
300
+ const text = buildToolActivityText(tool, { isError: failed });
301
+ if (text === '') return { written: false, reason: 'empty' };
302
+ const key = buildActivityKey({
303
+ userText: state.userText,
304
+ assistantText: state.assistantText,
305
+ tool,
306
+ });
307
+ const nowDate = now();
308
+ const nowMs = nowDate.getTime();
309
+ const refreshKey = `${state.sessionId}|${key}`;
310
+ const last = refreshes.get(refreshKey) ?? null;
311
+ const within = last !== null && Number.isFinite(nowMs) && nowMs - last.at < ACTIVITY_REFRESH_THROTTLE_MS;
312
+ const escalating = failed && (last === null || last.kind !== 'blocked');
313
+ if (within && !escalating) {
314
+ stats.throttled += 1;
315
+ return { written: false, reason: 'throttled' };
316
+ }
317
+ const kind = failed ? 'blocked' : classifyKind({ userText: state.userText, tool });
318
+ const result = store.append(
319
+ { sessionId: state.sessionId, kind, text, key, at: nowDate },
320
+ { touchSeq: true, refreshText: true },
321
+ );
322
+ if (!result.ok) {
323
+ if (result.code === 'E_PRIVACY') stats.privacy += 1;
324
+ else stats.errors += 1;
325
+ return { written: false, reason: result.code ?? 'error' };
326
+ }
327
+ refreshes.set(refreshKey, { at: nowMs, kind });
328
+ if (result.action === 'distilled') stats.distilled += 1;
329
+ else if (result.action === 'refreshed') stats.refreshed += 1;
330
+ else if (result.action === 'merged') stats.merged += 1;
331
+ else stats.written += 1;
332
+ return {
333
+ written: true,
334
+ action: result.action,
335
+ seq: result.seq,
336
+ seqBumped: result.seqBumped === true,
337
+ entry: result.entry,
338
+ };
339
+ }
340
+
341
+ function writeTurn(state, reason, turn) {
342
+ let text = buildActivityText({
343
+ userText: state.userText,
344
+ assistantText: state.assistantText,
345
+ tool: state.tool,
346
+ });
347
+ let kind = classifyKind({
348
+ userText: state.userText,
349
+ assistantText: state.assistantText,
350
+ tool: state.tool,
351
+ });
352
+ if (reason === 'failed') {
353
+ kind = 'blocked';
354
+ if (text === '') text = '回合失败(已记录为 blocked)';
355
+ }
356
+ if (text === '') {
357
+ stats.skipped += 1;
358
+ return { written: false, reason: 'empty' };
359
+ }
360
+ const candidate = {
361
+ sessionId: state.sessionId,
362
+ kind,
363
+ text,
364
+ key: buildActivityKey({
365
+ userText: state.userText,
366
+ assistantText: state.assistantText,
367
+ tool: state.tool,
368
+ }),
369
+ at: now(),
370
+ };
371
+ // A conclusion merge also carries new information for sibling sessions:
372
+ // bump seq and let the newest conclusion replace the turn's action text.
373
+ const result = store.append(candidate, { touchSeq: true, refreshText: true });
374
+ if (!result.ok) {
375
+ if (result.code === 'E_PRIVACY') stats.privacy += 1;
376
+ else stats.errors += 1;
377
+ return { written: false, reason: result.code ?? 'error', turn };
378
+ }
379
+ if (result.action === 'distilled') stats.distilled += 1;
380
+ else if (result.action === 'refreshed') stats.refreshed += 1;
381
+ else if (result.action === 'merged') stats.merged += 1;
382
+ else stats.written += 1;
383
+ return { written: true, action: result.action, seqBumped: result.seqBumped === true, entry: result.entry, turn };
384
+ }
385
+
386
+ function onSessionEvent(session, event) {
387
+ if (!enabled) return;
388
+ try {
389
+ const type = event?.type;
390
+ if (typeof type !== 'string') return;
391
+ const sessionId = sessionIdOfMessage(session, event);
392
+ if (type === 'session/disposed') {
393
+ if (sessionId !== '') {
394
+ sessions.delete(sessionId);
395
+ clearSessionRefreshes(sessionId);
396
+ }
397
+ return;
398
+ }
399
+ if (sessionId === '') return;
400
+ const state = stateFor(sessionId);
401
+
402
+ if (type === 'turn/start') {
403
+ const turn = event?.data?.turn;
404
+ state.turn = Number.isInteger(turn) ? turn : state.turn;
405
+ state.userText = '';
406
+ state.assistantText = '';
407
+ state.tool = null;
408
+ state.toolCalls = 0;
409
+ state.thinkingTurn = null;
410
+ state.thinkingWritten = false;
411
+ // A new turn is new information: do not let the previous turn's 10s
412
+ // window swallow its first tool action.
413
+ clearSessionRefreshes(sessionId);
414
+ return;
415
+ }
416
+
417
+ if (type === 'user/message') {
418
+ if (!isUserAuthoredMessage(event)) return;
419
+ const text = messageTextOf(event);
420
+ if (text !== '') state.userText = text;
421
+ return;
422
+ }
423
+
424
+ if (type === 'assistant/message') {
425
+ const data = event?.data ?? {};
426
+ const turn = data.turn;
427
+ if (Number.isInteger(turn) && Number.isInteger(state.turn) && turn !== state.turn) return;
428
+ const text = messageTextOf(event);
429
+ if (text !== '') state.assistantText = text;
430
+ // 1.8.0 thinking summaries: at most one row per session+turn; only when
431
+ // the injection switch is on. Visible reasoning text exists or nothing
432
+ // is written (no model call, no raw log rewrite).
433
+ if (thinkingInjection) {
434
+ const resolvedTurn = Number.isInteger(turn) ? turn : (Number.isInteger(state.turn) ? state.turn : null);
435
+ const alreadyWritten = state.thinkingWritten
436
+ && (state.thinkingTurn === resolvedTurn || resolvedTurn === null);
437
+ if (!alreadyWritten) {
438
+ const summary = summarizeThinking(extractThinkingText(data.message?.content));
439
+ if (summary !== '') {
440
+ const result = store.append(
441
+ {
442
+ sessionId,
443
+ kind: 'thinking',
444
+ text: summary,
445
+ key: `thinking|${resolvedTurn ?? 'turn'}`,
446
+ at: now(),
447
+ turnRef: resolvedTurn,
448
+ charCount: charLength(summary),
449
+ },
450
+ { touchSeq: true, refreshText: false },
451
+ );
452
+ if (result.ok) {
453
+ state.thinkingTurn = resolvedTurn;
454
+ state.thinkingWritten = true;
455
+ stats.thinkingWritten += 1;
456
+ } else if (result.code === 'E_PRIVACY') {
457
+ stats.privacy += 1;
458
+ } else {
459
+ stats.errors += 1;
460
+ }
461
+ }
462
+ }
463
+ }
464
+ return;
465
+ }
466
+
467
+ if (type === 'tool/call') {
468
+ state.toolCalls += 1;
469
+ state.tool = { ...toolSummaryOf(event?.data), isError: false };
470
+ // Event-level write: the sibling session sees the action while the
471
+ // turn is still running (throttled inside writeToolActivity).
472
+ stats.events += 1;
473
+ writeToolActivity(state);
474
+ return;
475
+ }
476
+
477
+ if (type === 'tool/result') {
478
+ const data = event?.data ?? {};
479
+ const message = data.message && typeof data.message === 'object' ? data.message : data;
480
+ const isError = message?.isError === true || data.isError === true;
481
+ if (isError) {
482
+ if (!state.tool && typeof data.name === 'string' && data.name.trim() !== '') {
483
+ state.tool = { ...toolSummaryOf(data), isError: true };
484
+ }
485
+ if (state.tool) {
486
+ state.tool.isError = true;
487
+ stats.events += 1;
488
+ writeToolActivity(state, { isError: true });
489
+ }
490
+ }
491
+ return;
492
+ }
493
+
494
+ if (type === 'turn/end') {
495
+ const turn = event?.data?.turn;
496
+ const reason = normalizeOrigin(event?.data?.reason?.kind);
497
+ const resolved = Number.isInteger(turn) ? turn : state.turn;
498
+ stats.turns += 1;
499
+ if (reason === 'completed' || reason === 'failed') {
500
+ writeTurn(state, reason, resolved);
501
+ } else {
502
+ stats.skipped += 1;
503
+ }
504
+ state.userText = '';
505
+ state.assistantText = '';
506
+ state.tool = null;
507
+ state.toolCalls = 0;
508
+ state.turn = Number.isInteger(resolved) ? resolved : null;
509
+ }
510
+ } catch {
511
+ stats.errors += 1;
512
+ }
513
+ }
514
+
515
+ function skip(reason) {
516
+ stats.skipped += 1;
517
+ return { injected: false, skipped: reason };
518
+ }
519
+
520
+ /**
521
+ * One pre-step attempt. Returns {injected, skipped?, entries?}; never throws.
522
+ */
523
+ async function maybeInject(payload) {
524
+ if (!enabled) return { injected: false, skipped: 'disabled' };
525
+ if (Number(payload?.step) !== 1) return { injected: false, skipped: 'not-step-1' };
526
+ const sessionId = preStepSessionId(payload);
527
+ if (sessionId === '') return skip('no-session');
528
+ if (normalizeOrigin(headerOf(payload).origin) === 'subagent') return skip('subagent');
529
+ const messages = payload?.messages;
530
+ if (!Array.isArray(messages)) return skip('no-messages');
531
+ if (!messages.some((message) => isHumanMessage(message))) return skip('empty-turn');
532
+ if (messages.some((message) => {
533
+ const text = textOfMessage(message);
534
+ return text.includes(ACTIVITY_HINT_MARK) || text.includes(THINKING_HINT_MARK);
535
+ })) {
536
+ return { injected: false, skipped: 'turn-already-injected' };
537
+ }
538
+
539
+ const read = store.read();
540
+ if (!read.ok) return skip('board-unreadable');
541
+ const state = readActivityState(memoryRoot, sessionId);
542
+ const boardMax = maxSeqOf(read.entries);
543
+ // Board reset (deleted/emptied/replaced-from-backup): the persisted seq-only
544
+ // cursor can sit above every current row, so `seq > lastSeenSeq` would match
545
+ // nothing forever. Falling back to 0 replays the current board instead of
546
+ // stranding the sibling (RE-R15.5). The store's high-water sidecar normally
547
+ // prevents this; this branch is the safety net for legacy/reset boards.
548
+ const resetCursor = boardMax > 0 && state.lastSeenSeq > boardMax;
549
+ if (resetCursor) stats.resets += 1;
550
+ const effectiveSeen = resetCursor ? 0 : state.lastSeenSeq;
551
+ const fresh = selectActivities(read.entries, {
552
+ selfSessionId: sessionId,
553
+ lastSeenSeq: effectiveSeen,
554
+ maxItems: cfg.maxItems,
555
+ });
556
+ // 1.8.0: thinking summaries are selected separately and never mixed into
557
+ // the normal activity lines (the cursor is the same lastSeenSeq).
558
+ const freshThinking = thinkingInjection
559
+ ? selectThinkingActivities(read.entries, {
560
+ selfSessionId: sessionId,
561
+ lastSeenSeq: effectiveSeen,
562
+ maxItems: 5,
563
+ })
564
+ : [];
565
+ if (fresh.length === 0 && freshThinking.length === 0) {
566
+ return { injected: false, skipped: 'no-new', reset: resetCursor };
567
+ }
568
+
569
+ const allowed = [];
570
+ for (const item of fresh) {
571
+ try {
572
+ const verdict = privacyScan(item.text);
573
+ if (!verdict || verdict.verdict === 'local_only') {
574
+ stats.privacy += 1;
575
+ continue;
576
+ }
577
+ } catch {
578
+ stats.privacy += 1;
579
+ continue;
580
+ }
581
+ allowed.push(item);
582
+ }
583
+ const allowedThinking = [];
584
+ for (const item of freshThinking) {
585
+ const summary = summarizeThinking(item.text);
586
+ if (summary === '' || !isThinkingPayloadSafe(summary)) {
587
+ stats.privacy += 1;
588
+ continue;
589
+ }
590
+ try {
591
+ const verdict = privacyScan(summary);
592
+ if (!verdict || verdict.verdict === 'local_only') {
593
+ stats.privacy += 1;
594
+ continue;
595
+ }
596
+ } catch {
597
+ stats.privacy += 1;
598
+ continue;
599
+ }
600
+ allowedThinking.push({ ...item, text: summary });
601
+ }
602
+ if (allowed.length === 0 && allowedThinking.length === 0) {
603
+ return { injected: false, skipped: 'privacy' };
604
+ }
605
+
606
+ const parts = [];
607
+ if (allowed.length > 0) {
608
+ const rendered = renderActivityHint(allowed, {
609
+ maxChars: cfg.injectMaxChars,
610
+ maxItems: cfg.maxItems,
611
+ nowMs: now().getTime(),
612
+ });
613
+ if (rendered.text !== '') parts.push(rendered.text);
614
+ }
615
+ if (allowedThinking.length > 0) {
616
+ const thinkingText = renderThinkingHint(allowedThinking, { maxItems: 5 });
617
+ if (thinkingText !== '') parts.push(thinkingText);
618
+ }
619
+ if (parts.length === 0) return skip('budget-empty');
620
+ const text = parts.join('\n\n');
621
+
622
+ const createUserMessage = await resolveCreateUserMessage(cfg.createUserMessage);
623
+ if (typeof createUserMessage !== 'function') return skip('no-create-user-message');
624
+ let message;
625
+ try {
626
+ message = assertInjectionSource(createUserMessage({
627
+ role: 'user',
628
+ content: [{ type: 'text', text }],
629
+ source: injectionSource(),
630
+ }));
631
+ } catch {
632
+ return skip('create-message-failed');
633
+ }
634
+ const index = insertionIndex(messages);
635
+ if (index <= 0) return skip('no-human-message');
636
+ try {
637
+ messages.splice(index, 0, message);
638
+ } catch {
639
+ return skip('splice-failed');
640
+ }
641
+
642
+ const injectedItems = [...allowed, ...allowedThinking];
643
+ const nextState = {
644
+ ...state,
645
+ lastSeenSeq: Math.max(effectiveSeen, maxSeqOf(read.entries)),
646
+ lastInjectedSeq: maxSeqOf(injectedItems),
647
+ lastInjectedAt: now().toISOString(),
648
+ injectedTurns: state.injectedTurns + 1,
649
+ resets: state.resets + (resetCursor ? 1 : 0),
650
+ lastResetAt: resetCursor ? now().toISOString() : state.lastResetAt,
651
+ updatedAt: now().toISOString(),
652
+ };
653
+ try {
654
+ writeJsonAtomic(activityStateFile(memoryRoot, sessionId), nextState);
655
+ } catch {
656
+ /* state is an optimization; a failed write costs a repeat, not a turn */
657
+ }
658
+ stats.injected += 1;
659
+ stats.thinkingInjected += allowedThinking.length;
660
+ return {
661
+ injected: true,
662
+ sessionId,
663
+ chars: charLength(text),
664
+ reset: resetCursor,
665
+ items: injectedItems.map((item) => ({ seq: item.seq, sessionId: item.sessionId, kind: item.kind })),
666
+ thinkingItems: allowedThinking.length,
667
+ text,
668
+ };
669
+ }
670
+
671
+ function install(ctx) {
672
+ if (!enabled || !ctx || typeof ctx.on !== 'function') return false;
673
+ try {
674
+ const dispose = ctx.on('session/event', (session, event) => onSessionEvent(session, event));
675
+ if (typeof dispose === 'function') disposers.push(dispose);
676
+ } catch (error) {
677
+ stats.errors += 1;
678
+ logger(`[activity] session/event 监听注册失败:${error?.code ?? error?.message ?? error}`);
679
+ }
680
+ try {
681
+ const dispose = ctx.on('agent/pre-step', async (payload, next) => {
682
+ // B3: shared phase = shared-brain injection only (timing only).
683
+ const sessionId = preStepSessionId(payload);
684
+ const phase = startPrestepPhase(sessionId, payload?.turn, 'shared');
685
+ try {
686
+ await maybeInject(payload);
687
+ } catch {
688
+ stats.errors += 1;
689
+ notePrestepDegrade(sessionId, payload?.turn, { kind: 'error', phase: 'shared', detail: 'E_ACTIVITY_PRE_STEP' });
690
+ } finally {
691
+ phase.end();
692
+ }
693
+ return typeof next === 'function' ? next() : undefined;
694
+ });
695
+ if (typeof dispose === 'function') disposers.push(dispose);
696
+ } catch (error) {
697
+ stats.errors += 1;
698
+ logger(`[activity] agent/pre-step 监听注册失败:${error?.code ?? error?.message ?? error}`);
699
+ }
700
+ return true;
701
+ }
702
+
703
+ function dispose() {
704
+ while (disposers.length > 0) {
705
+ const disposer = disposers.pop();
706
+ try {
707
+ disposer();
708
+ } catch {
709
+ /* already disposed */
710
+ }
711
+ }
712
+ sessions.clear();
713
+ refreshes.clear();
714
+ }
715
+
716
+ return {
717
+ enabled,
718
+ memoryRoot,
719
+ config: { ...cfg, thinkingInjection },
720
+ stats,
721
+ store,
722
+ install,
723
+ dispose,
724
+ onSessionEvent,
725
+ maybeInject,
726
+ readState: (sessionId) => (enabled ? readActivityState(memoryRoot, sessionId) : emptyActivityState(sessionId)),
727
+ boardFile: store?.boardFile ?? null,
728
+ };
729
+ }
730
+
731
+ /**
732
+ * Register the two hooks for the feature switch. A disabled stream registers
733
+ * nothing and touches no file; a broken registration is a warning only.
734
+ */
735
+ export function registerActivityStream(ctx, cfg = {}, options = {}) {
736
+ const stream = createActivityStream({
737
+ enabled: cfg.enabled !== false,
738
+ memoryRoot: options.memoryRoot,
739
+ injectMaxChars: cfg.injectMaxChars,
740
+ maxItems: cfg.maxItems,
741
+ thinkingInjection: cfg.thinkingInjection === true,
742
+ createUserMessage: cfg.createUserMessage,
743
+ now: options.now,
744
+ privacyScan: options.privacyScan,
745
+ logger: options.logger,
746
+ });
747
+ if (!stream.enabled) return stream;
748
+ stream.install(ctx);
749
+ if (typeof ctx?.effect === 'function') {
750
+ try {
751
+ ctx.effect(() => () => stream.dispose(), 'longlive.shared-brain-activity');
752
+ } catch {
753
+ /* a fake context may not own an effect scope */
754
+ }
755
+ }
756
+ return stream;
757
+ }
758
+
759
+ export { ACTIVITY_HINT_MARK, ACTIVITY_REFRESH_THROTTLE_MS, shortSessionId, activityBoardFile };
760
+ export { THINKING_HINT_MARK, THINKING_KIND, THINKING_LINE_MAX, THINKING_MAX_ITEMS } from './thinking.js';