dsh-ops 0.2.3 → 0.2.5-beta.1

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 (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/PROVENANCE.md +24 -0
  3. package/README.en.md +51 -9
  4. package/README.md +36 -11
  5. package/README.zh.md +36 -11
  6. package/cordis.patch.yml +27 -155
  7. package/docs/auto-compact.md +102 -0
  8. package/docs/manual-validation.md +10 -5
  9. package/docs/schema-current.json +20 -13
  10. package/docs/schema-measurement.md +29 -9
  11. package/dsh-plugin.json +40 -7
  12. package/lib/components/background/icon.png +0 -0
  13. package/lib/components/background/index.js +3 -0
  14. package/lib/components/background/locale/en.json +6 -0
  15. package/lib/components/background/locale/zh.json +6 -0
  16. package/lib/components/background/package.json +6 -0
  17. package/lib/components/file/icon.png +0 -0
  18. package/lib/components/file/index.js +3 -0
  19. package/lib/components/file/locale/en.json +6 -0
  20. package/lib/components/file/locale/zh.json +6 -0
  21. package/lib/components/file/package.json +6 -0
  22. package/lib/components/shell/icon.png +0 -0
  23. package/lib/components/shell/index.js +3 -0
  24. package/lib/components/shell/locale/en.json +6 -0
  25. package/lib/components/shell/locale/zh.json +6 -0
  26. package/lib/components/shell/package.json +6 -0
  27. package/lib/index.js +46 -32
  28. package/lib/policy.js +16 -8
  29. package/lib/profile-install.js +1 -1
  30. package/lib/runtime-pool.js +42 -0
  31. package/lib/session-pwsh.js +31 -0
  32. package/lib/tools.js +64 -16
  33. package/node_modules/@dsh-ops/auto-compact/LICENSE +30 -0
  34. package/node_modules/@dsh-ops/auto-compact/README.md +23 -0
  35. package/node_modules/@dsh-ops/auto-compact/lib/client.js +79 -0
  36. package/node_modules/@dsh-ops/auto-compact/lib/config.js +60 -0
  37. package/node_modules/@dsh-ops/auto-compact/lib/index.js +132 -0
  38. package/node_modules/@dsh-ops/auto-compact/locale/en.json +6 -0
  39. package/node_modules/@dsh-ops/auto-compact/locale/zh.json +6 -0
  40. package/node_modules/@dsh-ops/auto-compact/package.json +31 -0
  41. package/package.json +27 -4
  42. package/packages/README.md +87 -0
  43. package/packages/auto-compact/LICENSE +30 -0
  44. package/packages/auto-compact/README.md +23 -0
  45. package/packages/auto-compact/lib/client.js +79 -0
  46. package/packages/auto-compact/lib/config.js +60 -0
  47. package/packages/auto-compact/lib/index.js +132 -0
  48. package/packages/auto-compact/locale/en.json +6 -0
  49. package/packages/auto-compact/locale/zh.json +6 -0
  50. package/packages/auto-compact/package.json +31 -0
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "dsh-ops",
3
- "version": "0.2.3",
3
+ "version": "0.2.5-beta.1",
4
4
  "publishConfig": {
5
- "access": "public"
5
+ "access": "public",
6
+ "tag": "beta"
6
7
  },
7
8
  "repository": {
8
9
  "type": "git",
@@ -39,7 +40,22 @@
39
40
  "default": "./lib/policy.js"
40
41
  },
41
42
  "./package.json": "./package.json",
42
- "./locale/*.json": "./locale/*.json"
43
+ "./locale/*.json": "./locale/*.json",
44
+ "./shell": {
45
+ "default": "./lib/components/shell/index.js"
46
+ },
47
+ "./shell/package.json": "./lib/components/shell/package.json",
48
+ "./shell/locale/*.json": "./lib/components/shell/locale/*.json",
49
+ "./file": {
50
+ "default": "./lib/components/file/index.js"
51
+ },
52
+ "./file/package.json": "./lib/components/file/package.json",
53
+ "./file/locale/*.json": "./lib/components/file/locale/*.json",
54
+ "./background": {
55
+ "default": "./lib/components/background/index.js"
56
+ },
57
+ "./background/package.json": "./lib/components/background/package.json",
58
+ "./background/locale/*.json": "./lib/components/background/locale/*.json"
43
59
  },
44
60
  "bin": {
45
61
  "dsh-ops": "bin/dsh-ops.mjs"
@@ -51,6 +67,7 @@
51
67
  },
52
68
  "files": [
53
69
  "lib",
70
+ "packages/auto-compact",
54
71
  "bin",
55
72
  "cordis.patch.yml",
56
73
  "dsh-plugin.json",
@@ -97,8 +114,8 @@
97
114
  }
98
115
  },
99
116
  "optionalDependencies": {
100
- "@dsh-ops/fastctx-win32-x64": "0.2.1",
101
117
  "@dsh-ops/bash-win32-x64": "0.2.1",
118
+ "@dsh-ops/fastctx-win32-x64": "0.2.1",
102
119
  "@dsh-ops/pwsh-win32-x64": "0.2.1"
103
120
  },
104
121
  "devDependencies": {
@@ -112,5 +129,11 @@
112
129
  },
113
130
  "engines": {
114
131
  "node": "^22.19 || >=24"
132
+ },
133
+ "bundleDependencies": [
134
+ "@dsh-ops/auto-compact"
135
+ ],
136
+ "dependencies": {
137
+ "@dsh-ops/auto-compact": "file:packages/auto-compact"
115
138
  }
116
139
  }
@@ -0,0 +1,87 @@
1
+ # Platform package skeletons
2
+
3
+ The three directories here are the **skeletons** of the platform packages this
4
+ plugin publishes, one per package, named `<kind>-<platform>-<arch>`. They are
5
+ not the same kind of package:
6
+
7
+ | Skeleton | Published as | What it carries |
8
+ | --- | --- | --- |
9
+ | `fastctx-win32-x64/` | the FastCtx package for Windows x64 | a payload: `bin/fastctx.exe`, built from `vendor/fastctx`, plus the Apache-2.0 licence and notice documents |
10
+ | `bash-win32-x64/` | the bash pin package for Windows x64 | metadata only: a pin on Git for Windows PortableGit |
11
+ | `pwsh-win32-x64/` | the pwsh pin package for Windows x64 | metadata only: a pin on PowerShell 7 |
12
+
13
+ Each skeleton holds only what is committed by hand: the package metadata, this
14
+ documentation, and — for the FastCtx package — the licence and notice documents
15
+ that must travel with the binary. **No binary is ever committed here.**
16
+
17
+ ## What the build puts in each package
18
+
19
+ `scripts/build-platform-packages.mjs` writes every package to
20
+ `<dist>/stage/<kind>-<platform>-<arch>/`.
21
+
22
+ The **payload** package:
23
+
24
+ ```text
25
+ fastctx-win32-x64/
26
+ ├── package.json name and version injected, the rest from the skeleton
27
+ ├── provenance.json source, toolchain, upstream revision, payload digest
28
+ ├── bin/fastctx.exe the payload
29
+ ├── LICENSE, NOTICE, FORK.md, carried verbatim from vendor/fastctx, as Apache-2.0
30
+ ├── UPSTREAM.md, requires of a redistributor
31
+ │ THIRD_PARTY_LICENSES*.md
32
+ └── README.md
33
+ ```
34
+
35
+ The **pin** packages:
36
+
37
+ ```text
38
+ bash-win32-x64/ (and pwsh-win32-x64/)
39
+ ├── package.json name and version injected, the rest from the skeleton
40
+ ├── provenance.json the pin: repository, tag, asset name, URL, sha256,
41
+ │ bytes, the measured unpacked size (files and bytes),
42
+ │ unpacked layout, entry point and its digest, licence,
43
+ │ copyright, and the statement that this package
44
+ │ forwards no upstream bytes
45
+ └── README.md
46
+ ```
47
+
48
+ Two rules keep a pin a pin, and both are enforced rather than asserted:
49
+
50
+ - **The stage may contain nothing else.** After assembly the script lists every
51
+ file in a pin package's stage and fails if any file is not one of
52
+ `package.json`, `provenance.json`, `README.md`. `--check` re-runs the same
53
+ test on the existing stages, so an upstream byte cannot slip into a pin
54
+ without failing a gate.
55
+ - **The pin is data, not a copy of the digests in prose.** `provenance.json` is
56
+ generated from the pin record — today read from `lib/shells.js` when that
57
+ module exports a pin table or pin function, and otherwise from the measured
58
+ fallback inside the build script, which the script announces on every run and
59
+ `--check` prints. `--verify-upstream` re-derives each asset digest from the
60
+ live upstream release and deletes the download again, so a stale pin is one
61
+ command away from being caught.
62
+
63
+ ## Where the names come from
64
+
65
+ **The package name lives in exactly one module.** `lib/shells.js`
66
+ (`PLATFORM_PACKAGES`) owns the shell names and therefore the scope; the FastCtx
67
+ name comes from `lib/binary.js` (`opsFastctxPackage`). The build script imports
68
+ them and never spells a name out, which is why the skeleton `package.json` files
69
+ carry neither `name` nor `version` — a re-scoped release is one edit in `lib/`,
70
+ not a search-and-replace here.
71
+
72
+ The version of a platform package is always the version of the main package
73
+ (`package.json`), injected at build time so the two can never disagree.
74
+
75
+ ## Only the payload package is injected
76
+
77
+ The main package's published copy declares the FastCtx platform package as an
78
+ `optionalDependency` — injected by the build script, never in the committed
79
+ manifest, where a version nobody has published yet would make every
80
+ contributor's `npm install` a 404.
81
+
82
+ The two shell pins are published but **never injected**, because installing one
83
+ alone cannot work: a pin carries no shell, so a dependency on it would install
84
+ metadata where a deployment expects a binary. They exist for a deployment that
85
+ deliberately resolves a shell — the plugin's own provisioning path, or an
86
+ explicit install — and an install on a platform they do not cover skips them
87
+ instead of failing.
@@ -0,0 +1,30 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 T-Auto and the dsh-ops contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ -------------------------------------------------------------------------------
24
+
25
+ This MIT license covers the dsh-ops plugin: everything in this repository
26
+ except `vendor/fastctx/`, which is FastCtx under the Apache License 2.0. The
27
+ distribution as a whole is therefore licensed under the composite
28
+ `MIT AND Apache-2.0`. See `NOTICE` for the full statement and the attribution
29
+ FastCtx requires, and `vendor/fastctx/LICENSE-APACHE` for the Apache License
30
+ 2.0 text.
@@ -0,0 +1,23 @@
1
+ # @dsh-ops/auto-compact
2
+
3
+ Internal, bundled companion of dsh-ops. It adds one independently toggled row to
4
+ **Plugins → dsh-ops → Included components**, not a second marketplace bundle.
5
+ The package-root entry is required by DSH rc.2's browser module discovery; plugin
6
+ subpaths are not browser entry points. No separate installation is required.
7
+
8
+ Defaults: threshold 50% (integer 1–99), cooldown 120s (30–3600), timeout 120s
9
+ (10–600). Configuration belongs to this row and uses host profile settings with
10
+ revision fencing and restore defaults. The policy calls the same public idle
11
+ maintenance API as `/compact` only above the rounded Web context percentage and
12
+ after a non-cancelled turn. Running tools are not interrupted. No new model-facing
13
+ tool, polling, or host auto-policy modification is introduced.
14
+
15
+ One owned operation at a time, cooldown, unchanged-surface suppression and
16
+ abort/drain teardown bound work. Commit/save failures suspend this session until
17
+ component reload and require inspection. Timeouts rely on cooperative cancellation.
18
+ Summarization may call the host's configured model and incur cost; it is not
19
+ lossless. Missing samples/capacity and busy admission skip work.
20
+
21
+ This companion is carried by the dsh-ops 0.2.5-beta.1 prerelease candidate, not the
22
+ stable 0.2.4. Check registry metadata for publication status. See the main repository's
23
+ `docs/auto-compact.md` for safety limits and manual acceptance requirements.
@@ -0,0 +1,79 @@
1
+ /** Browser artifact in the host's closure-factory module format; no globals except its loader. */
2
+ window.__ModuleLoader__.load({ id: '@dsh-ops/auto-compact', factory: require => {
3
+ const { createElement: h, useState, useEffect, useRef } = require('react')
4
+ const NS = 'dshOps.autoCompact'
5
+ const dictionaries = {
6
+ en: {
7
+ title: 'Settings', threshold: 'Context threshold (%)', cooldown: 'Cooldown (seconds)', timeout: 'Compaction timeout (seconds)',
8
+ save: 'Save', reset: 'Restore defaults', saved: 'Saved', failed: 'Not saved. Configuration may have changed; reload and retry.',
9
+ unavailable: 'Enable this component to configure it. This client must have host settings write access.',
10
+ hint: 'When the Web context meter is strictly above the threshold, compact at the next idle boundary using the same service as /compact. Running tools are not interrupted. Summarization may call your configured model and incur cost. No extra model-facing tool is added.',
11
+ invalid: 'Enter integers: threshold 1–99, cooldown 30–3600, timeout 10–600.',
12
+ },
13
+ zh: {
14
+ title: '设置', threshold: '上下文阈值(%)', cooldown: '冷却时间(秒)', timeout: '压缩超时(秒)',
15
+ save: '保存', reset: '恢复默认', saved: '已保存', failed: '未保存,配置可能已变化,请重新加载后重试。',
16
+ unavailable: '请先启用此组件;当前客户端还需具有宿主设置写入权限。',
17
+ hint: 'Web 上下文占用严格高于阈值后,在下一次空闲边界调用与 /compact 相同的服务;不会打断正在运行的工具。摘要可能调用已配置模型并产生费用,不额外增加模型可调用工具。',
18
+ invalid: '请输入整数:阈值 1–99,冷却 30–3600 秒,超时 10–600 秒。',
19
+ },
20
+ }
21
+ const defaults = { thresholdPercent: 50, cooldownSeconds: 120, timeoutSeconds: 120 }
22
+ const fields = [
23
+ ['thresholdPercent', 'threshold', 1, 99],
24
+ ['cooldownSeconds', 'cooldown', 30, 3600],
25
+ ['timeoutSeconds', 'timeout', 10, 600],
26
+ ]
27
+ function Settings({ view, form, t }) {
28
+ const [draft, setDraft] = useState(defaults)
29
+ const [revision, setRevision] = useState(undefined)
30
+ const [busy, setBusy] = useState(false)
31
+ const [message, setMessage] = useState('')
32
+ const alive = useRef(false)
33
+ useEffect(() => { alive.current = true; return () => { alive.current = false } }, [])
34
+ useEffect(() => {
35
+ if (form?.state.status === 'ready') {
36
+ setDraft({ ...defaults, ...form.state.value })
37
+ setRevision(form.state.revision)
38
+ }
39
+ }, [form?.state.revision, form?.state.status])
40
+ if (view === 'summary') return t('hint')
41
+ const writable = form?.state.status === 'ready' && form.state.writable && form.state.mode === 'host'
42
+ async function save(reset) {
43
+ if (!writable || busy || revision === undefined) return
44
+ if (!reset && fields.some(([key, , min, max]) => !Number.isInteger(Number(draft[key])) || String(draft[key]).trim() === '' || Number(draft[key]) < min || Number(draft[key]) > max)) {
45
+ setMessage(t('invalid')); return
46
+ }
47
+ setBusy(true); setMessage('')
48
+ try {
49
+ const ops = fields.map(([key]) => reset
50
+ ? { op: 'unset', path: [key] }
51
+ : { op: 'set', path: [key], value: Number(draft[key]) })
52
+ const ok = await form.mutate(ops, revision)
53
+ if (alive.current) setMessage(t(ok ? 'saved' : 'failed'))
54
+ } catch { if (alive.current) setMessage(t('failed')) }
55
+ finally { if (alive.current) setBusy(false) }
56
+ }
57
+ return h('section', { 'data-dsh-ops-auto-compact-settings': true },
58
+ h('h4', null, t('title')),
59
+ h('p', null, t('hint')),
60
+ !writable ? h('p', { role: 'status' }, t('unavailable')) : null,
61
+ ...fields.map(([key, label, min, max]) => h('label', { key, style: { display: 'block', marginBottom: 12 } },
62
+ t(label), ' ', h('input', {
63
+ type: 'number', min, max, step: 1, value: draft[key], disabled: !writable || busy,
64
+ onChange: event => setDraft(current => ({ ...current, [key]: event.target.value })),
65
+ }))),
66
+ h('button', { type: 'button', disabled: !writable || busy, onClick: () => save(false) }, t('save')), ' ',
67
+ h('button', { type: 'button', disabled: !writable || busy, onClick: () => save(true) }, t('reset')),
68
+ h('p', { role: 'status' }, message))
69
+ }
70
+ return {
71
+ inject: ['slots', 'locale'],
72
+ apply(ctx) {
73
+ ctx.effect(() => ctx.get('locale').register(NS, dictionaries), 'dsh-ops settings locales')
74
+ ctx.get('slots').inject('plugins.row.config', () => ctx.get('slots').register({
75
+ name: 'plugins.row.config', key: 'dsh-ops#dsh-ops-auto-compact', locale: NS,
76
+ }, Settings))
77
+ },
78
+ }
79
+ } })
@@ -0,0 +1,60 @@
1
+ /** Dependency-free strict settings and structural host form descriptor. */
2
+ export const AUTO_COMPACT_DEFAULTS = Object.freeze({ thresholdPercent: 50, cooldownSeconds: 120, timeoutSeconds: 120 })
3
+ const LIMITS = Object.freeze({ thresholdPercent: [1, 99], cooldownSeconds: [30, 3600], timeoutSeconds: [10, 600] })
4
+ const WRITE = Symbol.for('cosmokit.volatile.write')
5
+
6
+ export function resolveAutoCompactConfig(input = {}) {
7
+ if (input === null || typeof input !== 'object' || Array.isArray(input)
8
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) {
9
+ throw new TypeError('dsh-ops auto-compact config must be a plain object')
10
+ }
11
+ for (const key of Object.keys(input)) {
12
+ if (!Object.hasOwn(LIMITS, key)) throw new TypeError(`dsh-ops auto-compact: unknown config key ${key}`)
13
+ }
14
+ return Object.fromEntries(Object.entries(AUTO_COMPACT_DEFAULTS).map(([key, fallback]) => {
15
+ const raw = input[key]
16
+ const value = raw !== null && typeof raw === 'object' && WRITE in raw ? raw.get() : raw === undefined ? fallback : raw
17
+ const [min, max] = LIMITS[key]
18
+ if (!Number.isInteger(value) || value < min || value > max) {
19
+ throw new TypeError(`dsh-ops auto-compact: ${key} must be an integer between ${min} and ${max}`)
20
+ }
21
+ return [key, value]
22
+ }))
23
+ }
24
+
25
+ /**
26
+ * Standard Schema validation plus a serializable structural descriptor, not a
27
+ * Schemastery instance or vendor impersonation. The host rehydrates with its OWN
28
+ * library. No loading-time host values are imported. Live references implement
29
+ * the shared cosmokit.volatile.write protocol and are validated before updates.
30
+ */
31
+ export function createAutoCompactConfigSchema() {
32
+ const dict = Object.fromEntries(Object.entries(AUTO_COMPACT_DEFAULTS).map(([key, value]) => [key, {
33
+ type: 'number',
34
+ meta: { default: value, min: LIMITS[key][0], max: LIMITS[key][1], step: 1, volatile: true },
35
+ toJSON() { return { type: this.type, meta: { ...this.meta } } },
36
+ }]))
37
+ return {
38
+ type: 'object', meta: { default: {} }, dict,
39
+ '~standard': {
40
+ version: 1, vendor: 'dsh-ops',
41
+ validate(input) {
42
+ try {
43
+ const value = Object.fromEntries(Object.entries(resolveAutoCompactConfig(input)).map(([key, initial]) => {
44
+ let current = initial
45
+ return [key, Object.freeze({ get: () => current, [WRITE]: next => { current = next } })]
46
+ }))
47
+ return { value }
48
+ } catch (error) { return { issues: [{ message: error.message }] } }
49
+ },
50
+ },
51
+ toJSON() {
52
+ const refs = { 0: { type: 'object', meta: { default: {} }, dict: {} } }
53
+ Object.entries(dict).forEach(([key, node], index) => {
54
+ refs[0].dict[key] = index + 1
55
+ refs[index + 1] = { type: node.type, meta: { ...node.meta } }
56
+ })
57
+ return { uid: 0, refs }
58
+ },
59
+ }
60
+ }
@@ -0,0 +1,132 @@
1
+ /** Idle-only policy over the same public compaction seam as /compact. */
2
+ import { createAutoCompactConfigSchema, resolveAutoCompactConfig } from './config.js'
3
+
4
+ export const name = 'dsh-ops-auto-compact'
5
+ export const inject = ['compaction', 'sessionProjections']
6
+ export const Config = createAutoCompactConfigSchema()
7
+
8
+ /** Match the Web context meter; absent/invalid samples never trigger work. */
9
+ export function contextPercent(pressure) {
10
+ const used = pressure?.projectedTokens ?? pressure?.pressureTokens
11
+ const capacity = pressure?.contextWindow
12
+ if (!Number.isFinite(used) || used < 0 || !Number.isFinite(capacity) || capacity <= 0) return null
13
+ return Math.min(100, Math.round(used / capacity * 100))
14
+ }
15
+
16
+ /** Surface changes, not compaction bookkeeping, permit a fresh attempt. */
17
+ function surfaceKey(session) {
18
+ return JSON.stringify([session.surface.contentGeneration, session.surface.nodes])
19
+ }
20
+
21
+ /** Bounded, event-driven supervisor. It never publishes a model-callable tool. */
22
+ export function createAutoCompactor(ctx, rawConfig, {
23
+ now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout,
24
+ } = {}) {
25
+ const states = new WeakMap()
26
+ const active = new Map()
27
+ const disposedAgents = new WeakSet()
28
+ let closed = false
29
+ // One summary at a time for this component, across all sessions.
30
+ let globalNextAt = 0
31
+
32
+ function check(agent) {
33
+ if (closed || disposedAgents.has(agent) || agent.status !== 'idle' || active.size > 0) return
34
+ let config, percent, key
35
+ try {
36
+ config = resolveAutoCompactConfig(rawConfig)
37
+ percent = contextPercent(ctx.get('sessionProjections').snapshot(agent.session, ['contextPressure']).values.contextPressure)
38
+ key = surfaceKey(agent.session)
39
+ } catch {
40
+ // No guessed capacity, no admission when a required service disappears.
41
+ return
42
+ }
43
+ if (percent === null || percent <= config.thresholdPercent) return
44
+ const state = states.get(agent.session)
45
+ const time = now()
46
+ if (time < globalNextAt || time < (state?.nextAt ?? 0)
47
+ || state?.blocked
48
+ || state?.key === key && state.threshold === config.thresholdPercent) return
49
+
50
+ const controller = new AbortController()
51
+ const slot = { controller, done: undefined }
52
+ // Install before calling compactNow: service admission may emit synchronously.
53
+ active.set(agent, slot)
54
+ states.set(agent.session, { key, threshold: config.thresholdPercent, nextAt: time + config.cooldownSeconds * 1000 })
55
+ globalNextAt = time + config.cooldownSeconds * 1000
56
+ const timer = setTimer(() => controller.abort(new Error('Automatic compaction timed out')), config.timeoutSeconds * 1000)
57
+ timer.unref?.()
58
+ let blocked = false
59
+ slot.done = (async () => {
60
+ try {
61
+ const result = await ctx.get('compaction').compactNow(agent, controller.signal)
62
+ if (result !== null) ctx.logger.info('dsh-ops auto-compact: compacted idle session history')
63
+ } catch (error) {
64
+ const code = error?.code
65
+ if (code === 'commit' || code === 'persistence') {
66
+ blocked = true
67
+ ctx.logger.warn('dsh-ops auto-compact: session requires inspection after a commit/save failure; automatic compaction is suspended until this component is reloaded')
68
+ } else if (!controller.signal.aborted && code !== 'busy' && code !== 'cancelled') {
69
+ // Do not log history, provider errors, paths or credentials.
70
+ ctx.logger.warn('dsh-ops auto-compact: compaction failed; automatic retry is backed off')
71
+ }
72
+ } finally {
73
+ clearTimer(timer)
74
+ // Suppress retries against our own replacement or bookkeeping records.
75
+ try {
76
+ states.set(agent.session, {
77
+ key: surfaceKey(agent.session), threshold: config.thresholdPercent, blocked,
78
+ nextAt: now() + config.cooldownSeconds * 1000,
79
+ })
80
+ } catch { /* disposed session */ }
81
+ globalNextAt = now() + config.cooldownSeconds * 1000
82
+ if (active.get(agent) === slot) active.delete(agent)
83
+ }
84
+ })()
85
+ }
86
+
87
+ function retire(agent) {
88
+ disposedAgents.add(agent)
89
+ active.get(agent)?.controller.abort(new Error('Agent disposed'))
90
+ }
91
+
92
+ async function dispose() {
93
+ closed = true
94
+ const slots = [...active.values()]
95
+ for (const slot of slots) slot.controller.abort(new Error('Automatic compaction component disabled'))
96
+ await Promise.allSettled(slots.map(slot => slot.done))
97
+ }
98
+ return { check, retire, dispose }
99
+ }
100
+
101
+ /** Keep this plugin's validated live references stable through Fiber.update. */
102
+ export function installLiveConfigUpdates(ctx, config) {
103
+ ctx.on('internal/update', (candidate, _noSave, _next) => {
104
+ const values = resolveAutoCompactConfig(candidate)
105
+ const write = Symbol.for('cosmokit.volatile.write')
106
+ for (const [key, value] of Object.entries(values)) config[key][write](value)
107
+ try { ctx.emit('loader/volatile-update', Object.keys(values).map(key => [key])) }
108
+ catch { ctx.logger.warn('dsh-ops auto-compact: live config notification failed') }
109
+ })
110
+ }
111
+
112
+ /** Scoped listeners, abort and drain follow the host's teardown lifecycle. */
113
+ export function apply(ctx, config = {}, { liveUpdates = true } = {}) {
114
+ resolveAutoCompactConfig(config)
115
+ if (liveUpdates) installLiveConfigUpdates(ctx, config)
116
+ const policy = createAutoCompactor(ctx, config)
117
+ const lastTurnSignals = new WeakMap()
118
+ ctx.effect(function* () {
119
+ yield () => policy.dispose()
120
+ yield ctx.on('agent/status', ({ agent, status }) => {
121
+ if (status === 'idle' && !lastTurnSignals.get(agent)?.aborted) policy.check(agent)
122
+ })
123
+ yield ctx.on('agent/turn-stopping', ({ agent, signal }) => {
124
+ lastTurnSignals.set(agent, signal)
125
+ })
126
+ yield ctx.on('agent/disposed', ({ agent }) => policy.retire(agent))
127
+ // Hot-enabling also covers already loaded idle sessions, without waking them.
128
+ ctx.inject(['agents'], inner => {
129
+ for (const agent of inner.get('agents').list()) policy.check(agent)
130
+ })
131
+ }, 'dsh-ops auto-compact lifecycle')
132
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Automatic context compaction",
4
+ "description": "Compact idle context above a configurable threshold (50% by default), without interrupting running tasks."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "上下文自动压缩",
4
+ "description": "上下文占用高于可设置阈值(默认 50%)时,在会话空闲后自动压缩,避免打断运行中的任务。"
5
+ }
6
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@dsh-ops/auto-compact",
3
+ "version": "0.1.0",
4
+ "description": "Idle-only automatic context compaction and row settings for dsh-ops.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json",
12
+ "./locale/*.json": "./locale/*.json"
13
+ },
14
+ "dsh": {
15
+ "client": {
16
+ "platform": "web",
17
+ "inject": [
18
+ "@deepseek-ai/dsh-client-locale",
19
+ "@deepseek-ai/dsh-client-ui-settings",
20
+ "@deepseek-ai/dsh-client-ui-plugin-manager"
21
+ ]
22
+ }
23
+ },
24
+ "files": ["lib", "locale", "LICENSE", "README.md"],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/T-Auto/dsh-ops.git",
28
+ "directory": "packages/auto-compact"
29
+ },
30
+ "publishConfig": { "access": "public" }
31
+ }