@try-works/dsh-memory 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@try-works/dsh-memory",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Git-backed, workspace-scoped memory for the DeepSeek Harness (global layer + per-workspace layers)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -97,6 +97,30 @@ window.__ModuleLoader__.load({
97
97
  ' opacity: 0.6;',
98
98
  ' cursor: default;',
99
99
  '}',
100
+ '.dsm-subtitle {',
101
+ ' margin: 4px 0 2px;',
102
+ ' font-size: 13px;',
103
+ ' line-height: 20px;',
104
+ ' font-weight: 500;',
105
+ ' color: var(--dsw-alias-label-primary);',
106
+ '}',
107
+ '.dsm-switch-label {',
108
+ ' display: flex;',
109
+ ' align-items: center;',
110
+ ' gap: 8px;',
111
+ ' cursor: pointer;',
112
+ '}',
113
+ '.dsm-check {',
114
+ ' width: 14px;',
115
+ ' height: 14px;',
116
+ ' margin: 0;',
117
+ ' flex-shrink: 0;',
118
+ ' accent-color: var(--dsw-alias-brand-primary);',
119
+ '}',
120
+ '.dsm-check:disabled {',
121
+ ' opacity: 0.6;',
122
+ ' cursor: default;',
123
+ '}',
100
124
  '.dsm-actions {',
101
125
  ' display: flex;',
102
126
  ' align-items: center;',
@@ -648,6 +672,37 @@ window.__ModuleLoader__.load({
648
672
  { key: 'gitToken', label: 'Access token (optional)', kind: 'secret', hint: 'Personal access token for pushing. Stored by the host and never sent back to this page, so the field is always empty: type a new token to replace it, and leave it empty to keep the current one.' },
649
673
  ]
650
674
 
675
+ /**
676
+ * Asset-store fields, in their own card. Sizes are EDITED in MB/GB but WRITTEN in bytes,
677
+ * because the config stores exact byte counts and nobody should have to type 1073741824.
678
+ * The order matters: the master switch comes first, then the classes it governs.
679
+ */
680
+ const ASSET_UNITS = { MB: 1024 * 1024, GB: 1024 * 1024 * 1024 }
681
+ const ASSET_FIELDS = [
682
+ { key: 'assetsEnabled', kind: 'switch', defaultOn: false, label: 'Store files as assets', hint: 'Master switch. While this is off every asset tool refuses and nothing is captured; turning it off is never a deletion, stored assets stay readable and searchable.' },
683
+ { key: 'assetsDocuments', kind: 'switch', defaultOn: true, label: 'Documents', hint: 'PDF, DOCX, PPTX, TXT and Markdown files.' },
684
+ { key: 'assetsMedia', kind: 'switch', defaultOn: true, label: 'Media', hint: 'PNG, JPEG, WebP and GIF. An image attached to a message is stored automatically when this is on.' },
685
+ { key: 'assetsData', kind: 'switch', defaultOn: true, label: 'Data', hint: 'CSV, TSV, JSON and XLSX files.' },
686
+ { key: 'assetsDescribe', kind: 'switch', defaultOn: true, label: 'Describe each capture', hint: 'One model call writes what the file is and when to read it. Off keeps the deterministic fallback only.' },
687
+ { key: 'assetsMaxBytes', kind: 'bytes', unit: 'MB', defaultBytes: 50 * 1024 * 1024, label: 'Maximum file size (MB)', hint: 'A larger file is refused with its size, not truncated.' },
688
+ { key: 'assetsWorkspaceQuotaBytes', kind: 'bytes', unit: 'GB', defaultBytes: 1024 * 1024 * 1024, label: 'Workspace quota (GB)', hint: 'Total asset bytes per workspace. Over quota a capture asks you what to do; nothing is pruned.' },
689
+ { key: 'assetsGlobalQuotaBytes', kind: 'bytes', unit: 'GB', defaultBytes: 1024 * 1024 * 1024, label: 'Global quota (GB)', hint: 'Total asset bytes in the global scope.' },
690
+ ]
691
+
692
+ /** A saved byte count in the unit the page shows; '' when there is no saved value. */
693
+ function assetSizeValue(bytes, unit) {
694
+ const n = Number(bytes)
695
+ if (!Number.isFinite(n) || n <= 0) return ''
696
+ return String(Math.round((n / ASSET_UNITS[unit]) * 100) / 100)
697
+ }
698
+
699
+ /** The page's unit back to bytes; undefined when the input is not a usable number. */
700
+ function assetSizeBytes(text, unit) {
701
+ const n = Number(text)
702
+ if (!Number.isFinite(n) || n < 0) return undefined
703
+ return Math.round(n * ASSET_UNITS[unit])
704
+ }
705
+
651
706
  // The settings namespace is the plugin's ENTRY ID, not its package name. The bundle patch mounts
652
707
  // the package @try-works/dsh-memory as the row `dsh-memory`, while a hand-written row may use
653
708
  // a short id like `memory`. Both must work, so the candidates are tried in order and
@@ -716,6 +771,45 @@ window.__ModuleLoader__.load({
716
771
  react.createElement('p', { className: 'dsm-field-help' }, field.hint))
717
772
  }
718
773
 
774
+ /** One labelled checkbox. A switch reads the DRAFT first, then the saved value. */
775
+ function switchField(field, value, draft, edit, disabled) {
776
+ // An unset value falls back to the SCHEMA default, not to alse: showing the three
777
+ // classes as off while the config has them on would describe the plugin wrongly.
778
+ const saved = value[field.key] === undefined ? field.defaultOn === true : value[field.key] === true
779
+ const checked = draft[field.key] !== undefined ? draft[field.key] === true : saved
780
+ return react.createElement('div', { className: 'dsm-field', key: field.key },
781
+ react.createElement('label', { className: 'dsm-switch-label', htmlFor: 'dsm-' + field.key },
782
+ react.createElement('input', {
783
+ id: 'dsm-' + field.key,
784
+ className: 'dsm-check',
785
+ type: 'checkbox',
786
+ checked,
787
+ disabled,
788
+ onChange: (event) => edit(field.key, event.target.checked),
789
+ }),
790
+ react.createElement('span', { className: 'dsm-field-label' }, field.label)),
791
+ react.createElement('p', { className: 'dsm-field-help' }, field.hint))
792
+ }
793
+
794
+ /** One byte-size field, shown in MB or GB and written back in bytes on save. */
795
+ function bytesField(field, value, draft, edit, disabled) {
796
+ const savedBytes = value[field.key] === undefined ? field.defaultBytes : value[field.key]
797
+ const shown = draft[field.key] !== undefined ? draft[field.key] : assetSizeValue(savedBytes, field.unit)
798
+ return react.createElement('div', { className: 'dsm-field', key: field.key },
799
+ react.createElement('label', { className: 'dsm-field-label', htmlFor: 'dsm-' + field.key }, field.label),
800
+ react.createElement('input', {
801
+ id: 'dsm-' + field.key,
802
+ className: 'dsm-input',
803
+ type: 'number',
804
+ min: '0',
805
+ step: '1',
806
+ value: shown,
807
+ disabled,
808
+ onChange: (event) => edit(field.key, event.target.value),
809
+ }),
810
+ react.createElement('p', { className: 'dsm-field-help' }, field.hint))
811
+ }
812
+
719
813
  /** The model picker: the configured catalog plus "session default" and any stale value. */
720
814
  function modelField(value, draft, edit, disabled, models) {
721
815
  const selected = fieldValue('summaryModel', value, draft)
@@ -792,13 +886,17 @@ window.__ModuleLoader__.load({
792
886
  setStatus({ kind: 'busy', text: 'Saving...' })
793
887
  try {
794
888
  let refused = false
795
- for (const field of FIELDS.concat([{ key: 'summaryModel' }])) {
889
+ for (const field of FIELDS.concat([{ key: 'summaryModel' }], ASSET_FIELDS)) {
796
890
  const next = draft[field.key]
797
891
  // An empty draft means 'not edited'. For a secret that is essential: writing an
798
892
  // empty value would erase the token, and writing the redacted snapshot value
799
893
  // would replace it with a placeholder.
800
- if (next === undefined || next === '' || next === value[field.key]) continue
801
- const accepted = await form.set(field.key, next)
894
+ if (next === undefined || next === '') continue
895
+ // A byte-size field is edited in MB/GB and stored in bytes; an unusable number is
896
+ // skipped rather than written as NaN.
897
+ const written = field.kind === 'bytes' ? assetSizeBytes(next, field.unit) : next
898
+ if (written === undefined || written === value[field.key]) continue
899
+ const accepted = await form.set(field.key, written)
802
900
  if (accepted === false) refused = true
803
901
  }
804
902
  setStatus(refused
@@ -823,6 +921,14 @@ window.__ModuleLoader__.load({
823
921
  if (field.kind === 'model') return modelField(value, draft, edit, disabled, models)
824
922
  return textField(field, value, draft, edit, disabled)
825
923
  })),
924
+ react.createElement('div', { className: 'dsm-card' },
925
+ react.createElement('h3', { className: 'dsm-subtitle' }, 'Asset store'),
926
+ react.createElement('p', { className: 'dsm-field-help' },
927
+ 'Files the agent keeps as memory: documents, media and data. Each entry records what the file is and when to read it; contents are never indexed. Bytes are stored once by content hash and synced with the store.'),
928
+ ...ASSET_FIELDS.map((field) => {
929
+ if (field.kind === 'switch') return switchField(field, value, draft, edit, disabled)
930
+ return bytesField(field, value, draft, edit, disabled)
931
+ })),
826
932
  react.createElement('div', { className: 'dsm-actions' },
827
933
  react.createElement('button', {
828
934
  className: 'dsm-button', type: 'button', disabled, onClick: save,
package/src/store.js CHANGED
@@ -7,7 +7,7 @@ import { GLOBAL_LAYERED, LAYER_DIRS, LAYERED } from './entry.js'
7
7
  import { sanitizeKey, deriveWorkspaceKey } from './workspace-key.js'
8
8
  import { workingPreamble } from './working.js'
9
9
 
10
- export const LAYER_FILES = ['INDEX.md', 'semantic.md', 'episodic.md', 'procedural.md', 'working.md', 'learnings.md', 'junk.md', 'codebase.md', 'history.md', 'decisions.md', 'state.md']
10
+ export const LAYER_FILES = ['INDEX.md', 'semantic.md', 'episodic.md', 'procedural.md', 'working.md', 'learnings.md', 'junk.md', 'codebase.md', 'history.md', 'decisions.md', 'assets.md', 'state.md']
11
11
 
12
12
  /** Working memory is the current-state head of ONE repo/workspace, never of the
13
13
  * whole store: a global working.md would mix unrelated repositories together. */
package/src/system-doc.js CHANGED
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url'
14
14
  import { LAYER_CODES, LAYER_DIRS, LAYERED, VOLATILITY_DAYS, WORKSPACE_ONLY_LAYERS } from './entry.js'
15
15
 
16
16
  /** Bumped whenever the described behaviour changes, so a stale on-disk copy is detectable. */
17
- export const SYSTEM_DOC_VERSION = 2
17
+ export const SYSTEM_DOC_VERSION = 3
18
18
 
19
19
  /** The template ships beside this module; resolve it relative to the module, not the cwd. */
20
20
  export function templatePath() {
@@ -83,6 +83,7 @@ const LAYER_PURPOSE = {
83
83
  codebase: 'What a module, file or subsystem of THIS repository does. Workspace only.',
84
84
  history: 'How the repository changed: one entry per commit, grouped by day. Workspace only.',
85
85
  decision: 'A settled choice and why it won: dependencies, conventions, architecture. Available in both scopes.',
86
+ asset: 'A stored file - a document, an image or a data file - documented with what it is and when to read it. The bytes live under assets/objects and are fetched with memory_asset_get; the contents are never indexed.',
86
87
  }
87
88
 
88
89
  /**