dsh-dynamic-context 0.1.1-rc.35 → 0.1.1-rc.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,7 @@
1
- # 0.1.1-rc.35 — DSH compatibility line
1
+ # 0.1.1-rc.36 — DSH compatibility line
2
2
 
3
- - Pin this build to its listed DSH releases and pair it with the other plugin on RC slot 5.
4
- - Use the native Session plugin record API. Adapt the Web read endpoint to the native Session lookup of this host generation.
3
+ - Pin this build to its listed DSH releases and pair it with the other plugin on RC slot 6.
4
+ - Persist plugin records in a separate JSONL-host-bound journal while validating native tool evidence against DSH persistence. Adapt the Web read endpoint to the native Session lookup of this host generation.
5
5
 
6
6
  # 0.1.0-rc.24 — task-graph prerequisite and graph provenance (2026-11-13)
7
7
 
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "updateRound": 3,
4
- "slot": 5,
5
- "branch": "0.2.1-alpha.2",
6
- "host": "0.2.1-alpha.2",
4
+ "slot": 6,
5
+ "branch": "0.2.1-alpha.1",
6
+ "host": "0.2.1-alpha.1",
7
7
  "hosts": [
8
- "0.2.1-alpha.2"
8
+ "0.2.1-alpha.1"
9
9
  ],
10
- "channel": "dsh-0.2.1-alpha.2",
11
- "taskgraph": "0.3.1-rc.35",
12
- "dynamicContext": "0.1.1-rc.35"
10
+ "channel": "dsh-0.2.1-alpha.1",
11
+ "taskgraph": "0.3.1-rc.36",
12
+ "dynamicContext": "0.1.1-rc.36"
13
13
  }
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # dsh-dynamic-context:动态上下文与受控请求通道
2
2
 
3
- 本构建版本为 `0.1.1-rc.35`。适用宿主和配套版本见 [兼容清单](DSH-COMPATIBILITY.json)。
3
+ 本构建版本为 `0.1.1-rc.36`。适用宿主和配套版本见 [兼容清单](DSH-COMPATIBILITY.json)。
4
4
 
5
- 分支 `0.2.1-alpha.2` 对应 npm 标签 `dsh-0.2.1-alpha.2`,适用于 DSH `0.2.1-alpha.2`,Node.js 要求为 `^22.19.0 || >=24.0.0`。请在同一 DSH profile 先安装 `dsh-bettertaskgraphs@0.3.1-rc.35`,再安装 `dsh-dynamic-context@0.1.1-rc.35`。Bundle 初装默认关闭,完成配置并启用后为新对话提供会话模式选择。
5
+ 分支 `0.2.1-alpha.1` 对应 npm 标签 `dsh-0.2.1-alpha.1`,适用于 DSH `0.2.1-alpha.1`,Node.js 要求为 `^22.19.0 || >=24.0.0`。请在同一 DSH profile 先安装 `dsh-bettertaskgraphs@0.3.1-rc.36`,再安装 `dsh-dynamic-context@0.1.1-rc.36`。Bundle 初装默认关闭,完成配置并启用后为新对话提供会话模式选择。
6
6
 
7
7
  前置插件:[dsh-bettertaskgraphs](https://github.com/oldcitynight/dsh-bettertaskgraphs)。开源协议为 GPL-3.0-only;复制的 DeepSeek 压缩策略另保留原 MIT 声明。验证范围和独立测试方法见 [VALIDATION.md](VALIDATION.md)。GitHub 源码发布与 npm 包发布是不同步骤,当前候选可从源码构建安装包。
8
8
 
@@ -17,7 +17,7 @@ TMPDIR="$PWD/.test-tmp" npm test
17
17
  npm run pack:bundle
18
18
  ```
19
19
 
20
- 源码构建依赖任务图 `0.3.1-rc.35`。该版本发布前,先将同一批次的任务图 tarball 加入 npm 缓存,再执行 npm ci。运行配置见 `examples/`。
20
+ 源码构建依赖任务图 `0.3.1-rc.36`。该版本发布前,先将同一批次的任务图 tarball 加入 npm 缓存,再执行 npm ci。运行配置见 `examples/`。
21
21
 
22
22
  候选包的独立支持范围是 direct。`native-guarded` 仅为开发对照,依赖另行安装的 context-guard wire hook;干净 DSH 没有该补丁时,在认领任务和创建执行 Agent 前拒绝运行。发布包不修改 DSH 适配器来启用该对照通道。
23
23
 
package/RELEASING.md CHANGED
@@ -1,6 +1,6 @@
1
- ## 0.1.1-rc.35 维护线
1
+ ## 0.1.1-rc.36 维护线
2
2
 
3
- DSH `0.2.1-alpha.2`。同批配对任务图 `0.3.1-rc.35` 和动态上下文 `0.1.1-rc.35`,npm 通道为 `dsh-0.2.1-alpha.2`。兼容清单随 Bundle 分发。
3
+ DSH `0.2.1-alpha.1`。同批配对任务图 `0.3.1-rc.36` 和动态上下文 `0.1.1-rc.36`,npm 通道为 `dsh-0.2.1-alpha.1`。兼容清单随 Bundle 分发。
4
4
 
5
5
  # 发布候选包 rc.24
6
6
 
package/VALIDATION.md CHANGED
@@ -1,4 +1,4 @@
1
- # Candidate 0.1.1-rc.35
1
+ # Candidate 0.1.1-rc.36
2
2
 
3
3
  See [the compatibility manifest](DSH-COMPATIBILITY.json) for the exact host versions and package pairing.
4
4
 
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  import fs from 'node:fs'
3
+ import { homedir } from 'node:os'
3
4
  import path from 'node:path'
4
5
  import { createHash } from 'node:crypto'
5
6
  import { AsyncLocalStorage } from 'node:async_hooks'
@@ -10,9 +11,9 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
10
11
  import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
11
12
  import { setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
12
13
  import { discoverBaselineInstructionFiles } from '@deepseek-ai/dsh-agent-instructions'
13
- import { resolveDshHome, resolveAgentsHome } from '@deepseek-ai/dsh-home-paths'
14
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
14
15
  import { createAssistantMessage, createSystemMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
15
- import { appendPluginRecord, Session } from '@deepseek-ai/dsh-session'
16
+ import { Session } from '@deepseek-ai/dsh-session'
16
17
  import { digest } from 'dsh-bettertaskgraphs/core'
17
18
  import { deepFreeze, textContent } from './request-policy.mjs'
18
19
  import { DynamicContextError, fail } from './core.mjs'
@@ -56,8 +57,8 @@ const nativeModules = {
56
57
  shell: ['@deepseek-ai/dsh-tool-bash', { enableRunInBackground: false, promoteOnTimeout: false }],
57
58
  web: ['@deepseek-ai/dsh-tool-web', {}],
58
59
  }
59
- const requiredServices = { filesystem: ['fs', 'workingDirectory'], search: ['subprocess', 'workingDirectory'],
60
- shell: ['shell', 'shellEnv', 'sandbox', 'workingDirectory'], web: ['web'], skills: ['skills'], state: [] }
60
+ const requiredServices = { filesystem: ['fs'], search: ['subprocess'],
61
+ shell: ['shell', 'shellEnv', 'sandbox'], web: ['web'], skills: ['skills'], state: [] }
61
62
  const readTools = new Set(['read', 'glob', 'grep', 'web_search', 'web_fetch', 'skill', 'dynamic_task_status', 'dynamic_context_source_read'])
62
63
  const modes = ['read-only', 'workspace-write', 'danger-full-access']
63
64
  const minimumMode = (...values) => modes[Math.min(...values.map(value => modes.indexOf(value)))]
@@ -72,18 +73,20 @@ const jsonTool = (name, description, parameters, execute) => defineTool({ name,
72
73
  export async function prepareDirectRuntime({ ctx, config, assembly, sessionId, sourceAgent, control, record, resumeState, sourceReader }) {
73
74
  const options = config.direct.runtime
74
75
  if (!options) return undefined
75
- for (const service of ['sandboxPolicy', 'approval', 'workingDirectory', ...options.modules.flatMap(name => requiredServices[name])]) {
76
+ for (const service of ['sandboxPolicy', 'approval', ...options.modules.flatMap(name => requiredServices[name])]) {
76
77
  if (!ctx.get(service)) fail('DIRECT_RUNTIME_MISSING_SERVICE', service)
77
78
  }
78
- const sandboxPolicy = ctx.get('sandboxPolicy'), approval = ctx.get('approval'), workingDirectory = ctx.get('workingDirectory')
79
+ const sandboxPolicy = ctx.get('sandboxPolicy'), approval = ctx.get('approval')
79
80
  const filesystem = ctx.get('fs'), shell = ctx.get('shell'), skills = ctx.get('skills'), sessions = ctx.get('sessions')
80
81
  const sandbox = ctx.get('sandbox'), subprocess = ctx.get('subprocess'), credentials = ctx.get('credentials')
81
82
  const root = fs.realpathSync(config.projectRoot), cwd = fs.realpathSync(path.resolve(root, options.cwd ?? '.'))
82
83
  if (!inside(root, cwd) || !fs.statSync(cwd).isDirectory()) fail('DIRECT_RUNTIME_INVALID_WORKSPACE')
83
84
  const stateDirectory = fs.realpathSync(config.directory)
84
85
  if (inside(root, stateDirectory)) fail('DIRECT_RUNTIME_STATE_INSIDE_WORKSPACE')
86
+ const sessionRecords = ctx.transactionGraph.sessionRecords
87
+ const recordRoot = sessionRecords.root(true)
85
88
  if (process.platform === 'linux' && options.modules.includes('shell') && inside('/tmp', stateDirectory)) fail('DIRECT_RUNTIME_STATE_IN_SHARED_TEMP')
86
- const instructionHomes = options.instructionHomes ?? { dshHome: resolveDshHome(), agentsHome: resolveAgentsHome() }
89
+ const instructionHomes = options.instructionHomes ?? { dshHome: resolveDshHome(), agentsHome: path.resolve(process.env.DSH_AGENTS_HOME || path.join(homedir(), '.agents')) }
87
90
  if (sourceAgent && ctx.agents.get(sourceAgent.id) !== sourceAgent) fail('DIRECT_RUNTIME_UNOWNED_SOURCE_AGENT')
88
91
  const ownerState = () => {
89
92
  const policy = sandboxPolicy.resolve(sourceAgent ? { session: sourceAgent.session } : {})
@@ -112,6 +115,18 @@ export async function prepareDirectRuntime({ ctx, config, assembly, sessionId, s
112
115
  if (!options.instructions) return []
113
116
  const files = await discoverBaselineInstructionFiles({ cwd: directory, projectRoot: root,
114
117
  ...instructionHomes, signal: control.signal })
118
+ // This host reads DSH_HOME/AGENTS.md and project rules. Include the
119
+ // second user-global file in the same pinned, bounded read as newer hosts.
120
+ const agentsRules = path.join(instructionHomes.agentsHome, 'AGENTS.md')
121
+ if (agentsRules !== path.join(instructionHomes.dshHome, 'AGENTS.md')
122
+ && !files.some(file => file.absolutePath === agentsRules)) {
123
+ try {
124
+ const info = fs.lstatSync(agentsRules)
125
+ if (!info.isFile()) fail('DIRECT_RUNTIME_INSTRUCTION_PATH_TYPE')
126
+ const global = files[0]?.absolutePath === path.join(instructionHomes.dshHome, 'AGENTS.md')
127
+ files.splice(global ? 1 : 0, 0, { absolutePath: agentsRules, displayPath: agentsRules })
128
+ } catch (error) { if (error.code !== 'ENOENT') throw error }
129
+ }
115
130
  const rows = []; let total = 0
116
131
  for (const file of files) {
117
132
  control.signal.throwIfAborted()
@@ -166,7 +181,7 @@ export async function prepareDirectRuntime({ ctx, config, assembly, sessionId, s
166
181
  if (options.protectPrivatePaths) privacy = createPrivatePathGuard({
167
182
  sandbox: options.modules.includes('shell') && assembly.tools.includes('bash') ? sandbox : undefined,
168
183
  subprocess: options.modules.includes('search') && assembly.tools.some(name => ['glob', 'grep'].includes(name)) ? subprocess : undefined,
169
- workspaceRoot: cwd, protectedRoots: [config.storageRoot ?? root, stateDirectory,
184
+ workspaceRoot: cwd, protectedRoots: [config.storageRoot ?? root, stateDirectory, recordRoot,
170
185
  path.join(config.storageRoot ?? root, '.dsh', 'workflow-witnesses-v1'), ...options.privateReadRoots],
171
186
  credentialFiles: [serviceIdentity(credentials)?.spec?.filename,
172
187
  path.join(resolveDshHome(), '.credentials.yaml'), path.join(instructionHomes.dshHome, '.credentials.yaml'),
@@ -221,7 +236,6 @@ export async function prepareDirectRuntime({ ctx, config, assembly, sessionId, s
221
236
  agentCtx.tools.restrict({ allow: assembly.tools })
222
237
  setSandboxMode(agent.session, policy.sandboxMode)
223
238
  setApprovalPolicy(agent.session, policy.approvalPolicy)
224
- appendPluginRecord(agent.session, 'plugin:dynamic-context/execution-host-v1', { taskId: assembly.task.id, contextVersion: assembly.contextVersion, policy })
225
239
  // The identity is native, but only this private runner owns its loop.
226
240
  for (const method of ['followup', 'inject', 'steer']) Object.defineProperty(agent, method, {
227
241
  value: () => fail('DIRECT_RUNTIME_NATIVE_MESSAGE_DISABLED'), configurable: false, writable: false,
@@ -230,6 +244,7 @@ export async function prepareDirectRuntime({ ctx, config, assembly, sessionId, s
230
244
  },
231
245
  }))
232
246
  const { agent } = handle
247
+ sessionRecords.append(agent.session, 'plugin:dynamic-context/execution-host-v1', { taskId: assembly.task.id, contextVersion: assembly.contextVersion, policy })
233
248
  const visible = ctx.tools.schemas(agent).map(row => row.name).sort()
234
249
  if (JSON.stringify(visible) !== JSON.stringify([...assembly.tools].sort())) fail('DIRECT_RUNTIME_TOOL_SCOPE_MISMATCH')
235
250
  const shellConfinement = assembly.tools.includes('bash') ? await sandbox.confine(['true'], {
@@ -283,12 +298,12 @@ export async function prepareDirectRuntime({ ctx, config, assembly, sessionId, s
283
298
  ]
284
299
  const systemPrompt = `${config.systemPrompt}\n\n${rules.join('\n')}\n\n<direct_host_facts>\n${JSON.stringify(facts)}\n</direct_host_facts>`
285
300
  const currentPolicy = () => ({ sandboxMode: sandboxPolicy.resolve({ session: agent.session }).mode,
286
- approvalPolicy: approval.effectivePolicy(agent.session), plan: policy.plan, cwd: workingDirectory.get(agent.session) })
301
+ approvalPolicy: approval.effectivePolicy(agent.session), plan: policy.plan, cwd: agent.session.header.cwd })
287
302
  const check = () => {
288
303
  control.signal.throwIfAborted()
289
304
  if (sourceAgent && ctx.agents.get(sourceAgent.id) !== sourceAgent) fail('DIRECT_RUNTIME_OWNER_DISPOSED')
290
305
  if (ctx.agents.get(agent.id) !== agent) fail('DIRECT_RUNTIME_EXECUTION_HOST_DISPOSED')
291
- for (const [name, service] of Object.entries({ sandboxPolicy, approval, workingDirectory, fs: filesystem, sandbox, subprocess, shell, skills, credentials }))
306
+ for (const [name, service] of Object.entries({ sandboxPolicy, approval, fs: filesystem, sandbox, subprocess, shell, skills, credentials }))
292
307
  if (serviceIdentity(ctx.get(name)) !== serviceIdentity(service)) fail('DIRECT_RUNTIME_SERVICE_CHANGED')
293
308
  privacy?.check()
294
309
  if (digest(ownerState()) !== digest(owner) || digest(currentPolicy()) !== digest(policy)) fail('DIRECT_RUNTIME_POLICY_CHANGED')
@@ -352,7 +367,7 @@ export async function prepareDirectRuntime({ ctx, config, assembly, sessionId, s
352
367
  const initial = new Set(initialInstructions.map(row => row.path)), required = new Set(facts.requiredSkills.map(row => row.name))
353
368
  return { instructions: [...instructionPins.values()].filter(row => !initial.has(row.path)), skills: [...loadedSkills.values()].filter(row => !required.has(row.name)) }
354
369
  },
355
- checkpoint(reference) { appendPluginRecord(agent.session, 'plugin:dynamic-context/checkpoint-v1', reference) },
370
+ checkpoint(reference) { sessionRecords.append(agent.session, 'plugin:dynamic-context/checkpoint-v1', reference) },
356
371
  async complete() { await verifySources(); completed = true; await close() },
357
372
  async preflight(calls) {
358
373
  await verifySources()
package/host.mjs CHANGED
@@ -11,7 +11,6 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
11
11
  import TokenMeter from '@deepseek-ai/dsh-token-meter'
12
12
  import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
13
13
  import Filesystem from '@deepseek-ai/dsh-fs-sandbox'
14
- import WorkingDirectory from '@deepseek-ai/dsh-working-directory'
15
14
  import Approval from '@deepseek-ai/dsh-user-approval'
16
15
  import Subprocess from '@deepseek-ai/dsh-subprocess-local'
17
16
  import Sandbox from '@deepseek-ai/dsh-sandbox-local'
@@ -46,7 +45,6 @@ export async function createDirectHost(input) {
46
45
  const runtime = config.direct.runtime
47
46
  await ctx.plugin(SandboxPolicy, { mode: runtime.sandboxMode, workspaceRoot: config.projectRoot })
48
47
  await ctx.plugin(Filesystem, { cwd: config.projectRoot })
49
- await ctx.plugin(WorkingDirectory, { defaultDirectory: config.projectRoot })
50
48
  await ctx.plugin(Approval, { policy: runtime.approvalPolicy })
51
49
  await ctx.plugin(Subprocess)
52
50
  await ctx.plugin(Sandbox)
package/index.mjs CHANGED
@@ -3,7 +3,7 @@ import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { createHash, randomUUID } from 'node:crypto'
5
5
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
6
- import { SessionId, appendPluginRecord } from '@deepseek-ai/dsh-session'
6
+ import { SessionId } from '@deepseek-ai/dsh-session'
7
7
  import { z } from 'zod'
8
8
  import { DynamicContextGraph, fail } from './core.mjs'
9
9
  import { digest } from 'dsh-bettertaskgraphs/core'
@@ -142,7 +142,7 @@ export class DynamicContextRuntime {
142
142
  }
143
143
  graphPresence() {
144
144
  const presence = this.graph.store.recoveryPlan()
145
- if (presence.state === 'NEVER_CREATED' && this.controller?.session.snapshotEvents().some(event =>
145
+ if (presence.state === 'NEVER_CREATED' && this.controller && this.taskGraph.sessionRecords.read(this.controller.session).some(({ event }) =>
146
146
  event.type === GRAPH_COMMIT_RECORD && event.ignorable && event.data.workflowId === this.workflowId))
147
147
  return { state: 'GRAPH_AND_WITNESS_LOST_AFTER_COMMIT', previouslyCommitted: true, recoveryRequired: true, canRestore: false }
148
148
  return presence
@@ -248,7 +248,7 @@ export class DynamicContextRuntime {
248
248
  if (this.config.transport === 'direct' && !this.config.direct.runtime && (binding.tools ?? []).some(tool => !this.config.direct.agentlessTools.includes(tool))) fail('DIRECT_TOOL_REQUIRES_EXPLICIT_AGENTLESS_PERMISSION')
249
249
  }
250
250
  const result = this.graph.submit(proposal)
251
- if (sourceAgent) appendPluginRecord(sourceAgent.session, GRAPH_COMMIT_RECORD, { workflowId: this.workflowId,
251
+ if (sourceAgent) this.taskGraph.sessionRecords.append(sourceAgent.session, GRAPH_COMMIT_RECORD, { workflowId: this.workflowId,
252
252
  graphId: result.graph.state.plan.id, firstChecksum: result.checksum, revision: result.revision })
253
253
  this.publish('created'); return result
254
254
  }
package/interaction.mjs CHANGED
@@ -2,7 +2,6 @@
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
5
- import { appendPluginRecord } from '@deepseek-ai/dsh-session'
6
5
  import { digest, normalizePlan } from 'dsh-bettertaskgraphs/core'
7
6
  import { connectTaskGraph } from './interop.mjs'
8
7
  import { registerControlTools } from './control-tools.mjs'
@@ -29,8 +28,8 @@ export class SessionInteraction {
29
28
  this.entries = new Map(); this.filters = new Map(); this.pending = new Set(); this.closed = false
30
29
  }
31
30
  record(agent) {
32
- return agent?.session.snapshotEvents().findLast(event => event.type === MODE_RECORD
33
- && event.ignorable === true && event.data.sessionId === agent.session.id)?.data
31
+ return agent && this.template.taskGraph.sessionRecords.read(agent.session).findLast(({ event }) => event.type === MODE_RECORD
32
+ && event.data.sessionId === agent.session.id)?.event.data
34
33
  }
35
34
  classic(agent) {
36
35
  if (!agent || this.ctx.agents.get(agent.id) !== agent) return null
@@ -43,7 +42,9 @@ export class SessionInteraction {
43
42
  linkedPresence(agent) {
44
43
  const entry = this.entries.get(agent)
45
44
  if (entry) return entry.runner.graphPresence()
46
- const events = agent.session.snapshotEvents()
45
+ let events
46
+ try { events = this.template.taskGraph.sessionRecords.read(agent.session).map(({ event }) => event) }
47
+ catch (error) { return { state: 'SESSION_PLUGIN_RECORDS_UNAVAILABLE', previouslyCommitted: null, code: error.code ?? error.name } }
47
48
  const key = digest({ id: agent.session.id, createdAt: agent.session.header.createdAt, cwd: agent.session.header.cwd })
48
49
  const committed = events.some(event => event.ignorable && event.type === GRAPH_COMMIT_RECORD
49
50
  && event.data.workflowId === `workflow_${key.slice(0, 40)}`)
@@ -74,7 +75,7 @@ export class SessionInteraction {
74
75
  text: ({ agent }) => {
75
76
  if (!agent || !ctx.agents.roots().includes(agent)) return ''
76
77
  const mode = this.mode(agent), entry = this.entries.get(agent)
77
- const state = { mode, selected: Boolean(this.record(agent)), taskGraphPresent: Boolean(this.classic(agent)), baseGraphPresence: this.basePresence(agent) }
78
+ const state = { mode, selected: mode === 'recovery' ? null : Boolean(this.record(agent)), taskGraphPresent: Boolean(this.classic(agent)), baseGraphPresence: this.basePresence(agent) }
78
79
  if (mode !== 'full' || !entry) return JSON.stringify(state)
79
80
  const runner = entry.runner
80
81
  return JSON.stringify({ ...state, workflowId: runner.config.interop.workflowId, status: runner.status() })
@@ -163,9 +164,10 @@ export class SessionInteraction {
163
164
  this.exact(agent); signal?.throwIfAborted()
164
165
  if (typeof reason !== 'string' || !reason.trim()) reject('DYNAMIC_CONTEXT_MODE_REASON_REQUIRED')
165
166
  if (this.pending.has(agent)) reject('DYNAMIC_CONTEXT_MODE_QUESTION_PENDING')
167
+ const baseGraphPresence = this.basePresence(agent)
168
+ if (!['PRESENT', 'CREATION_PENDING', 'NEVER_CREATED', 'OUTSIDE_SCOPE'].includes(baseGraphPresence.state))
169
+ return { status: 'RECOVERY_REQUIRED', mode: 'recovery', baseGraphPresence, changed: false }
166
170
  const prior = this.record(agent)
167
- if (!['PRESENT', 'CREATION_PENDING', 'NEVER_CREATED', 'OUTSIDE_SCOPE'].includes(this.basePresence(agent).state))
168
- return { status: 'RECOVERY_REQUIRED', mode: 'recovery', baseGraphPresence: this.basePresence(agent), changed: false }
169
171
  const graphPresence = this.entries.get(agent)?.runner.graphPresence()
170
172
  if (graphPresence?.recoveryRequired) return { status: 'RECOVERY_REQUIRED', mode: 'full', graphPresence, changed: false, nextTool: 'dynamic_context_recover_graphs' }
171
173
  if (prior && !reconsider) return { mode: prior.mode, alreadySelected: true }
@@ -186,7 +188,7 @@ export class SessionInteraction {
186
188
  const row = { version: 1, sessionId: agent.session.id, mode, evidence,
187
189
  ...(mode === 'full' && this.template.config.revision ? { policy: this.policy(agent) } : {}) }
188
190
  await this.release(agent)
189
- appendPluginRecord(agent.session, MODE_RECORD, row)
191
+ this.template.taskGraph.sessionRecords.append(agent.session, MODE_RECORD, row)
190
192
  if (!await this.ctx.sessions.flush(agent.session)) reject('DYNAMIC_CONTEXT_MODE_NOT_DURABLE')
191
193
  signal?.throwIfAborted(); this.exact(agent)
192
194
  if (mode === 'full') await this.mount(agent, row)
package/interop.mjs CHANGED
@@ -13,6 +13,7 @@ export function requireTaskGraph(ctx, expected) {
13
13
  if (!service || expected && identity(service) !== identity(expected) || service.host?.closed || service.workflows?.closed
14
14
  || service.workflowContract !== WORKFLOW_CONTRACT || service.extensionContract !== EXTENSION_CONTRACT
15
15
  || typeof service.registerWorkflowProvider !== 'function' || typeof service.registerSessionPolicy !== 'function'
16
+ || typeof service.sessionRecords?.read !== 'function' || typeof service.sessionRecords?.append !== 'function'
16
17
  || typeof service.createExtensionStore !== 'function' || typeof service.inspectGraphPresence !== 'function')
17
18
  throw new Error('DYNAMIC_CONTEXT_REQUIRES_ACTIVE_TASK_GRAPH')
18
19
  return service
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-dynamic-context",
3
- "version": "0.1.1-rc.35",
3
+ "version": "0.1.1-rc.36",
4
4
  "description": "Task-scoped direct model execution, native tools and durable context checkpoints for DeepSeek Harness",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",
@@ -64,7 +64,7 @@
64
64
  ],
65
65
  "engines": {
66
66
  "node": "^22.19.0 || >=24.0.0",
67
- "dsh": "0.2.1-alpha.2"
67
+ "dsh": "0.2.1-alpha.1"
68
68
  },
69
69
  "scripts": {
70
70
  "test": "node scripts/test.mjs",
@@ -79,41 +79,40 @@
79
79
  "zod": "4.6.7"
80
80
  },
81
81
  "peerDependencies": {
82
- "@deepseek-ai/dsh-fs": "0.2.1-alpha.2",
83
- "@deepseek-ai/dsh-credentials-local": "0.2.1-alpha.2",
84
- "@deepseek-ai/dsh-authorization": "0.2.1-alpha.2",
85
- "@deepseek-ai/dsh-deepseek-account-platform": "0.2.1-alpha.2",
86
- "dsh-bettertaskgraphs": "0.3.1-rc.35",
82
+ "@deepseek-ai/dsh-fs": "0.2.1-alpha.1",
83
+ "@deepseek-ai/dsh-credentials-local": "0.2.1-alpha.1",
84
+ "@deepseek-ai/dsh-authorization": "0.2.1-alpha.1",
85
+ "@deepseek-ai/dsh-deepseek-account-platform": "0.2.1-alpha.1",
86
+ "dsh-bettertaskgraphs": "0.3.1-rc.36",
87
87
  "@deepseek-ai/cordis": "4.0.5",
88
- "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
89
- "@deepseek-ai/dsh-agent-instructions": "0.2.1-alpha.2",
90
- "@deepseek-ai/dsh-home-paths": "0.2.1-alpha.2",
91
- "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
92
- "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
93
- "@deepseek-ai/dsh-scope": "0.2.1-alpha.2",
94
- "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
95
- "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
96
- "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.2",
97
- "@deepseek-ai/dsh-tool-fs": "0.2.1-alpha.2",
98
- "@deepseek-ai/dsh-tool-fs-search": "0.2.1-alpha.2",
99
- "@deepseek-ai/dsh-tool-bash": "0.2.1-alpha.2",
100
- "@deepseek-ai/dsh-tool-web": "0.2.1-alpha.2",
101
- "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2",
102
- "@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.2",
103
- "@deepseek-ai/dsh-agent-loop": "0.2.1-alpha.2",
104
- "@deepseek-ai/dsh-token-meter": "0.2.1-alpha.2",
105
- "@deepseek-ai/dsh-fs-sandbox": "0.2.1-alpha.2",
106
- "@deepseek-ai/dsh-working-directory": "0.2.1-alpha.2",
107
- "@deepseek-ai/dsh-subprocess-local": "0.2.1-alpha.2",
108
- "@deepseek-ai/dsh-sandbox-local": "0.2.1-alpha.2",
109
- "@deepseek-ai/dsh-bash-sandbox": "0.2.1-alpha.2",
110
- "@deepseek-ai/dsh-shell-env": "0.2.1-alpha.2",
111
- "@deepseek-ai/dsh-skill": "0.2.1-alpha.2",
112
- "@deepseek-ai/dsh-fs-observation-policy": "0.2.1-alpha.2",
113
- "@deepseek-ai/dsh-session-persistence-jsonl": "0.2.1-alpha.2",
114
- "@deepseek-ai/dsh-tool-call-timeout-policy": "0.2.1-alpha.2",
115
- "@deepseek-ai/dsh-llm-deepseek": "0.2.1-alpha.2",
116
- "@deepseek-ai/dsh-user-questions": "0.2.1-alpha.2"
88
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.1",
89
+ "@deepseek-ai/dsh-agent-instructions": "0.2.1-alpha.1",
90
+ "@deepseek-ai/dsh-home-paths": "0.2.1-alpha.1",
91
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.1",
92
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.1",
93
+ "@deepseek-ai/dsh-scope": "0.2.1-alpha.1",
94
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.1",
95
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.1",
96
+ "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.1",
97
+ "@deepseek-ai/dsh-tool-fs": "0.2.1-alpha.1",
98
+ "@deepseek-ai/dsh-tool-fs-search": "0.2.1-alpha.1",
99
+ "@deepseek-ai/dsh-tool-bash": "0.2.1-alpha.1",
100
+ "@deepseek-ai/dsh-tool-web": "0.2.1-alpha.1",
101
+ "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.1",
102
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.1",
103
+ "@deepseek-ai/dsh-agent-loop": "0.2.1-alpha.1",
104
+ "@deepseek-ai/dsh-token-meter": "0.2.1-alpha.1",
105
+ "@deepseek-ai/dsh-fs-sandbox": "0.2.1-alpha.1",
106
+ "@deepseek-ai/dsh-subprocess-local": "0.2.1-alpha.1",
107
+ "@deepseek-ai/dsh-sandbox-local": "0.2.1-alpha.1",
108
+ "@deepseek-ai/dsh-bash-sandbox": "0.2.1-alpha.1",
109
+ "@deepseek-ai/dsh-shell-env": "0.2.1-alpha.1",
110
+ "@deepseek-ai/dsh-skill": "0.2.1-alpha.1",
111
+ "@deepseek-ai/dsh-fs-observation-policy": "0.2.1-alpha.1",
112
+ "@deepseek-ai/dsh-session-persistence-jsonl": "0.2.1-alpha.1",
113
+ "@deepseek-ai/dsh-tool-call-timeout-policy": "0.2.1-alpha.1",
114
+ "@deepseek-ai/dsh-llm-deepseek": "0.2.1-alpha.1",
115
+ "@deepseek-ai/dsh-user-questions": "0.2.1-alpha.1"
117
116
  },
118
117
  "dsh": {
119
118
  "bundle": {
@@ -122,12 +121,12 @@
122
121
  },
123
122
  "publishConfig": {
124
123
  "access": "public",
125
- "tag": "dsh-0.2.1-alpha.2",
124
+ "tag": "dsh-0.2.1-alpha.1",
126
125
  "registry": "https://registry.npmjs.org/"
127
126
  },
128
127
  "devDependencies": {
129
- "@deepseek-ai/dsh": "0.2.1-alpha.2",
130
- "dsh-bettertaskgraphs": "0.3.1-rc.35"
128
+ "@deepseek-ai/dsh": "0.2.1-alpha.1",
129
+ "dsh-bettertaskgraphs": "0.3.1-rc.36"
131
130
  },
132
131
  "homepage": "https://github.com/oldcitynight/dsh-dynamic-context#readme",
133
132
  "repository": {
package/revision-host.mjs CHANGED
@@ -6,14 +6,14 @@ import { copy, digest, GraphError } from 'dsh-bettertaskgraphs/core'
6
6
  import { artifactProof } from './artifacts.mjs'
7
7
 
8
8
  export function revisionAuthority(ctx, agent) {
9
- const sessions = ctx.get?.('sessions') ?? ctx.sessions, workingDirectory = ctx.get?.('workingDirectory') ?? ctx.workingDirectory
9
+ const sessions = ctx.get?.('sessions') ?? ctx.sessions
10
10
  if (!agent || ctx.agents.get(agent.id) !== agent || sessions.get(agent.session.id) !== agent.session)
11
11
  throw new GraphError('REVISION_CONTROLLER_CHANGED')
12
12
  const sandbox = ctx.get('sandboxPolicy')?.resolve({ session: agent.session }), goal = ctx.get('goals')?.get(agent)
13
13
  const plan = ctx.get('runtimeQuality')?.planActive(agent)
14
14
  ?? ctx.get('agentPresets')?.serviceFor(agent, 'planMode')?.loggedActive(agent.session)
15
15
  ?? agent.ctx?.get('planMode')?.loggedActive(agent.session)
16
- return { cwd: workingDirectory.get(agent.session), header: agent.session.header,
16
+ return { cwd: ctx.get?.('workingDirectory')?.get(agent.session) ?? agent.session.header.cwd, header: agent.session.header,
17
17
  input: agent.session.deriveMessages().filter(message => ['user', 'developer', 'system'].includes(message.role)),
18
18
  sandbox: sandbox ? { mode: sandbox.mode, workspaceRoot: sandbox.workspaceRoot } : null,
19
19
  approval: ctx.get('approval')?.effectivePolicy(agent.session) ?? null, plan: Boolean(plan),
@@ -73,7 +73,9 @@ for (const mode of ['native', 'task-graph', 'full']) test(`human choice ${mode}
73
73
  assert.equal(schemaNames(f.h.ctx, first.agent).includes('dynamic_context_submit'), mode === 'full')
74
74
  assert.equal((await f.call(first.agent, 'dynamic_context_select_mode', { reason: '重复调用' })).alreadySelected, true)
75
75
  assert.equal(f.questions.length, 1)
76
- assert.ok(first.agent.session.snapshotEvents().some(row => row.type === MODE_RECORD && row.data.mode === mode))
76
+ const records = f.h.ctx.transactionGraph.sessionRecords
77
+ assert.ok(records.read(first.agent.session).some(({ event }) => event.type === MODE_RECORD && event.data.mode === mode))
78
+ assert.ok(!first.agent.session.snapshotEvents().some(event => event.type === MODE_RECORD))
77
79
  const secondPrompt = await f.h.ctx.systemPrompt.assemble({ agent: second.agent, scope: second.agent })
78
80
  assert.doesNotMatch(JSON.stringify(secondPrompt), /FIRST_TASK_CONTRACT|MUST_NOT_LEAK_TEMPLATE_GOAL/)
79
81
  if (mode === 'full') {
@@ -187,7 +189,7 @@ test('session commit receipt prevents fresh creation after both graph and witnes
187
189
  await f.call(agent, 'dynamic_context_select_mode', { reason: '项目任务' })
188
190
  await f.call(agent, 'dynamic_context_submit', { proposal: runtimeProposal([]) })
189
191
  const child = f.h.runner.interaction.entries.get(agent).runner
190
- assert.ok(agent.session.snapshotEvents().some(event => event.type === GRAPH_COMMIT_RECORD))
192
+ assert.ok(f.h.ctx.transactionGraph.sessionRecords.read(agent.session).some(({ event }) => event.type === GRAPH_COMMIT_RECORD))
191
193
  fs.unlinkSync(child.graph.store.file); fs.unlinkSync(child.graph.store.witness.file)
192
194
  assert.equal(child.graphPresence().state, 'GRAPH_AND_WITNESS_LOST_AFTER_COMMIT')
193
195
  assert.equal(child.graphPresence().canRestore, false)
@@ -279,3 +281,22 @@ for (const full of [false, true]) test(`native tool loop reaches ${full ? 'joint
279
281
  }
280
282
  assert.equal(f.questions.length, 1)
281
283
  })
284
+
285
+ test('legacy session record loss blocks mode changes while native JSONL remains readable', async t => {
286
+ const f = await fixture(t), { agent } = await f.agent('lost-plugin-records')
287
+ await f.call(agent, 'dynamic_context_select_mode', { reason: '项目任务' })
288
+ await f.call(agent, 'dynamic_context_submit', { proposal: runtimeProposal([]) })
289
+ const records = f.h.ctx.transactionGraph.sessionRecords
290
+ const native = await f.h.ctx.sessionPersistence.open(agent.id, 'read')
291
+ try {
292
+ const { events } = await native.read(0, 100)
293
+ assert.ok(events.some(row => row.type === 'user/message'))
294
+ assert.ok(events.every(row => !row.type.startsWith('plugin:')))
295
+ } finally { await native.close() }
296
+ fs.unlinkSync(path.join(records.directory(agent.session), 'records.json'))
297
+ const status = await f.call(agent, 'dynamic_context_graph_status', {})
298
+ assert.equal(status.mode, 'recovery')
299
+ assert.equal(status.base.code, 'LEGACY_SESSION_RECORD_JOURNAL_MISSING')
300
+ const selection = await f.call(agent, 'dynamic_context_select_mode', { reason: '尝试新建', reconsider: true })
301
+ assert.equal(selection.status, 'RECOVERY_REQUIRED')
302
+ })
@@ -12,7 +12,6 @@ import TaskGraph from 'dsh-bettertaskgraphs'
12
12
  import Persistence from '@deepseek-ai/dsh-session-persistence-jsonl'
13
13
  import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
14
14
  import Filesystem from '@deepseek-ai/dsh-fs-sandbox'
15
- import WorkingDirectory from '@deepseek-ai/dsh-working-directory'
16
15
  import * as DynamicContext from '../index.mjs'
17
16
  import { startFakeLlama } from './fake-llama.mjs'
18
17
 
@@ -50,7 +49,6 @@ export async function createHarness({ projectRoot, directory, respond, config =
50
49
  if (taskGraph && !ctx.get('transactionGraph')) {
51
50
  if (!ctx.get('sandboxPolicy')) await ctx.plugin(SandboxPolicy, { mode: 'workspace-write', workspaceRoot: projectRoot })
52
51
  if (!ctx.get('fs')) await ctx.plugin(Filesystem, { cwd: projectRoot })
53
- if (!ctx.get('workingDirectory')) await ctx.plugin(WorkingDirectory, { defaultDirectory: projectRoot })
54
52
  if (!ctx.get('sessionPersistence')) await ctx.plugin(Persistence, { root: directory + '/native-sessions' })
55
53
  await ctx.plugin(TaskGraph, { projectRoot })
56
54
  }
@@ -1,6 +1,5 @@
1
1
  import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
2
2
  import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox'
3
- import WorkingDirectory from '@deepseek-ai/dsh-working-directory'
4
3
  import Approval from '@deepseek-ai/dsh-user-approval'
5
4
  import Subprocess from '@deepseek-ai/dsh-subprocess-local'
6
5
  import Sandbox from '@deepseek-ai/dsh-sandbox-local'
@@ -21,7 +20,6 @@ export async function createRuntimeHarness({ projectRoot, directory, respond, ba
21
20
  setup: async ctx => {
22
21
  await ctx.plugin(SandboxPolicy, { mode, workspaceRoot: projectRoot })
23
22
  await ctx.plugin(SandboxedFileSystem, { cwd: projectRoot })
24
- await ctx.plugin(WorkingDirectory, { defaultDirectory: projectRoot })
25
23
  await ctx.plugin(Approval, { policy: approval })
26
24
  await ctx.plugin(Subprocess)
27
25
  await ctx.plugin(Sandbox)