dsh-biocore 0.1.7 → 0.1.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/cordis.patch.yml CHANGED
@@ -8,6 +8,8 @@
8
8
  dataDir: ''
9
9
  # Memory (task-203): capture both sides, inject <=420 chars, 20/hour.
10
10
  memoryEnabled: true
11
+ # Standalone session/event capture listener (off: capture happens in core turn_start/end)
12
+ memoryCaptureEnabled: false
11
13
  injectMaxChars: 420
12
14
  hourlyCap: 20
13
15
  # Proactive push (task-205): both switches must be true; default OFF.
@@ -19,4 +21,4 @@
19
21
  proactivePerHour: 2
20
22
  proactiveMinTrust: 0.6
21
23
  proactiveMaxChars: 60
22
- proactiveRequireOpenLoop: true
24
+ proactiveRequireOpenLoop: true
package/lib/index.js CHANGED
@@ -665,7 +665,7 @@ export function makeSessionEventListener(options) {
665
665
  }
666
666
  }
667
667
 
668
- const CORE_STATE_TTL_MS = 1500
668
+ const CORE_STATE_TTL_MS = 5000
669
669
  const CORE_STATE_CACHE = new Map()
670
670
 
671
671
  /** Read the core pipeline state action (panel state, task-212), briefly cached. */
@@ -726,33 +726,34 @@ export async function handleState(req, res, stateDir, options) {
726
726
  const sessionId = cleanSessionId(rawSession)
727
727
  if (!sessionId) return sendJson(res, 200, unavailable('bad_session'))
728
728
  const stateFile = sessionStateFile(stateDir, sessionId)
729
- let read = readStateFile(stateFile)
730
- let created = false
729
+ const read = readStateFile(stateFile)
730
+ let body = null
731
731
  if (read.missing) {
732
- const ensured = await ensureState(stateFile, personaProfilePathFor(stateDir))
733
- if (!ensured || ensured.ok !== true) {
734
- return sendJson(res, 200, unavailable('no_state_yet',
735
- { detail: (ensured && (ensured.reason || ensured.detail)) || 'ensure_failed' }))
736
- }
737
- created = true
738
- read = readStateFile(stateFile)
739
- }
740
- if (read.error) return sendJson(res, 200, unavailable(read.error))
741
- let body = projectState(read.state, { mtimeMs: read.mtimeMs, sessionId })
742
- // task-202: additive fields for the standalone instrument panel.
743
- Object.assign(body, buildPanelExtras(read.state))
744
- if (created) body.lazily_created = true
745
- // task-212: panel state comes from the core state action when a switch is on.
746
- try {
747
- const getSwitches = typeof opts.getSwitches === 'function' ? opts.getSwitches : null
748
- if (getSwitches && anySwitchOn(getSwitches())) {
749
- const core = await fetchCoreState(sessionId, {
750
- runAction: opts.runAction,
751
- store: opts.store,
752
- })
753
- body = mergeCoreState(body, core)
732
+ // Fast path: never call Python when the session file exists. Only a
733
+ // missing file + an enabled switch may fall back to runtimeAction('state').
734
+ let core = null
735
+ try {
736
+ const getSwitches = typeof opts.getSwitches === 'function' ? opts.getSwitches : null
737
+ if (getSwitches && anySwitchOn(getSwitches())) {
738
+ core = await fetchCoreState(sessionId, {
739
+ runAction: opts.runAction,
740
+ store: opts.store,
741
+ })
742
+ }
743
+ } catch { core = null }
744
+ if (!core || core.ok !== true || !core.state || typeof core.state !== 'object') {
745
+ return sendJson(res, 200, unavailable('no_state_yet'))
754
746
  }
755
- } catch { /* keep the file projection */ }
747
+ body = projectState(core.state, { mtimeMs: 0, sessionId })
748
+ Object.assign(body, buildPanelExtras(core.state))
749
+ body.source = 'runtime'
750
+ } else if (read.error) {
751
+ return sendJson(res, 200, unavailable(read.error))
752
+ } else {
753
+ body = projectState(read.state, { mtimeMs: read.mtimeMs, sessionId })
754
+ Object.assign(body, buildPanelExtras(read.state))
755
+ body.source = 'file'
756
+ }
756
757
  if (!payloadWithinLimit(body)) {
757
758
  return sendJson(res, 200, unavailable('payload_too_large'))
758
759
  }
@@ -867,10 +868,14 @@ export function apply(ctx, config) {
867
868
  const memoryDir = resolveMemoryDir(config)
868
869
  const injectionState = createInjectionState()
869
870
  try {
871
+ // Normally capture happens inside core turn_start/turn_end. The standalone
872
+ // session/event capture listener stays available but is opt-in.
873
+ if (config && config.memoryCaptureEnabled === true) {
870
874
  const capture = registerMemoryCapture(ctx, () => config, memoryDir, {
871
875
  injectionState,
872
876
  })
873
877
  if (capture) keep(capture.dispose)
878
+ }
874
879
  } catch { /* memory capture unavailable */ }
875
880
  try {
876
881
  const injection = registerMemoryInjection(ctx, () => config, memoryDir,
@@ -14,6 +14,8 @@
14
14
  * unavailable it falls back to the old adapter bridge once and logs a single
15
15
  * warning; all switches off means no python call, no prompt text, no gate.
16
16
  */
17
+ import { readFileSync } from 'node:fs'
18
+ import { join } from 'node:path'
17
19
  import { readStateFile, sessionStateFile } from './state.js'
18
20
  import { pendingSessionTurn, queueSessionTurn } from './turn-queue.js'
19
21
  import {
@@ -105,6 +107,13 @@ export function chooseCleanReply(original, coreCleanText, options) {
105
107
  return String(sanitize(source))
106
108
  }
107
109
 
110
+ export function personaApplied(dataDir) {
111
+ try {
112
+ const status = JSON.parse(readFileSync(join(String(dataDir || '').trim(), 'persona_preset.json'), 'utf8'))
113
+ return status && status.applied === true
114
+ } catch { return false }
115
+ }
116
+
108
117
  export function turnStartPayload(options) {
109
118
  const opts = options || {}
110
119
  const expression = opts.expression && typeof opts.expression === 'object'
@@ -114,6 +123,9 @@ export function turnStartPayload(options) {
114
123
  user_text: stringId(opts.userText).slice(0, 2000),
115
124
  switches: opts.switches || {},
116
125
  store: opts.store || defaultStoreDir(),
126
+ capture: true,
127
+ memoryEnabled: true,
128
+ persona_prompt: opts.personaPrompt !== false,
117
129
  length_hint: expression.length_hint === true,
118
130
  length_target: Number.isFinite(Number(expression.length_target))
119
131
  ? Number(expression.length_target) : EXPRESSION_DEFAULTS.length_target,
@@ -132,8 +144,8 @@ export function turnEndPayload(options) {
132
144
  user_text: stringId(opts.userText).slice(0, 2000),
133
145
  switches: opts.switches || {},
134
146
  store: opts.store || defaultStoreDir(),
135
- capture: false,
136
- memoryEnabled: false,
147
+ capture: true,
148
+ memoryEnabled: true,
137
149
  sanitize_reply: opts.sanitizeReply !== false,
138
150
  diagnostics,
139
151
  }, diagnostics, {
@@ -189,6 +201,7 @@ export function makeCoreSessionListener(options) {
189
201
  try {
190
202
  core = await runAction('turn_start', turnStartPayload({
191
203
  sessionId, userText: text, switches, store, expression: getExpression(),
204
+ personaPrompt: !personaApplied(stateDir),
192
205
  }))
193
206
  } catch (error) {
194
207
  core = { ok: false, reason: 'core_exception' }
@@ -347,4 +360,4 @@ function joinProfile(stateDir) {
347
360
  try {
348
361
  return stateDir ? String(stateDir).replace(/[\\/]+$/, '') + '/persona_profile.json' : ''
349
362
  } catch { return '' }
350
- }
363
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-biocore",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "BioCore DSH UI: per-session state tab, standalone instrument panel, /api/biocore/state, session/event one_turn updates, /bio sync.",
6
6
  "main": "lib/index.js",
@@ -1780,6 +1780,24 @@ def dispatch(action, payload=None):
1780
1780
  state_store = _relationship_store(data)
1781
1781
  if state_store is not None:
1782
1782
  _save_session_state(state_store, session_id, session_state)
1783
+ capture_enabled = _as_bool(data.get("capture"), True)
1784
+ captured_user = {"captured": False, "entry_id": "", "reason": "disabled"}
1785
+ capture_store = data.get("store")
1786
+ if capture_enabled and capture_store:
1787
+ user_capture = capture(session_id, "user", user_text, capture_store, {
1788
+ "enabled": _as_bool(data.get("memoryEnabled", True), True),
1789
+ "hourly_cap": data.get("hourly_cap", HOURLY_CAP),
1790
+ "min_chars": data.get("min_chars", MIN_CHARS),
1791
+ })
1792
+ decision = user_capture.get("decision") or {}
1793
+ entry = user_capture.get("entry") or {}
1794
+ captured_user = {
1795
+ "captured": bool(decision.get("capture")),
1796
+ "entry_id": str(entry.get("id") or ""),
1797
+ "reason": str(decision.get("reason") or ""),
1798
+ }
1799
+ elif capture_enabled:
1800
+ captured_user["reason"] = "no_store"
1783
1801
  block = prompt_block(dict(
1784
1802
  data, phase="turn", session_id=session_id,
1785
1803
  user_text=user_text,
@@ -1792,7 +1810,8 @@ def dispatch(action, payload=None):
1792
1810
  return {"ok": True, "prompt_block": block["text"],
1793
1811
  "constraints": self_layer_constraints(
1794
1812
  dict(data, user_text=user_text)),
1795
- "state": session_state}
1813
+ "state": session_state,
1814
+ "captured_user": captured_user}
1796
1815
  if action == "turn_end":
1797
1816
  session_id = str(data.get("session_id") or "").strip()
1798
1817
  if not session_id:
@@ -1828,13 +1847,22 @@ def dispatch(action, payload=None):
1828
1847
  gate["text"] = _sanitize.clean(gate.get("text") or "")
1829
1848
  except Exception:
1830
1849
  pass
1831
- captured = capture(session_id, "assistant", assistant_text, store, {
1832
- "enabled": _as_bool(data.get("memoryEnabled",
1833
- switches.get("memoryEnabled", True)),
1834
- True),
1835
- "hourly_cap": data.get("hourly_cap", HOURLY_CAP),
1836
- "min_chars": data.get("min_chars", MIN_CHARS),
1837
- })
1850
+ capture_enabled = _as_bool(data.get("capture"), True)
1851
+ capture_store = data.get("store")
1852
+ if capture_enabled and capture_store:
1853
+ captured = capture(session_id, "assistant", assistant_text,
1854
+ capture_store, {
1855
+ "enabled": _as_bool(data.get("memoryEnabled",
1856
+ switches.get("memoryEnabled", True)),
1857
+ True),
1858
+ "hourly_cap": data.get("hourly_cap", HOURLY_CAP),
1859
+ "min_chars": data.get("min_chars", MIN_CHARS),
1860
+ })
1861
+ else:
1862
+ captured = {"ok": True, "decision": {
1863
+ "capture": False,
1864
+ "reason": "disabled" if not capture_enabled else "no_store",
1865
+ }, "entry": None}
1838
1866
  merge_state_relationship_into_shared(session_id, data)
1839
1867
  decision = captured.get("decision") or {}
1840
1868
  entry = captured.get("entry") or {}
package/ui/panel.js CHANGED
@@ -7,7 +7,7 @@
7
7
  (function () {
8
8
  'use strict'
9
9
 
10
- var POLL_MS = 2000
10
+ var POLL_MS = 5000
11
11
  var SESSION = (new URLSearchParams(window.location.search).get('session') || '').trim()
12
12
  var SWITCHES = [
13
13
  { key: 'safety_enabled', label: '安全闸', desc: '打开后可能拦截/改写回复' },
@@ -1605,9 +1605,18 @@
1605
1605
  loadSeries()
1606
1606
  fetchState()
1607
1607
  fetchTrace()
1608
- window.setInterval(fetchState, POLL_MS)
1609
- window.setInterval(fetchTrace, POLL_MS)
1608
+ function pollTick() {
1609
+ if (document.hidden) return
1610
+ fetchState()
1611
+ fetchTrace()
1612
+ }
1613
+ window.setInterval(pollTick, POLL_MS)
1610
1614
  window.setInterval(updateStatusClock, 1000)
1615
+ document.addEventListener('visibilitychange', function () {
1616
+ if (document.hidden) return
1617
+ fetchState()
1618
+ fetchTrace()
1619
+ })
1611
1620
  window.addEventListener('resize', function () { renderChart(lastData) })
1612
1621
  }
1613
1622