@whaletalk/dsh-codex-collab 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -202,12 +202,17 @@ dsh-task --in "<继续推进的指令>" --session "session-8d481ad9-..."
202
202
 
203
203
  ## 已知限制
204
204
 
205
- - 网关插件需要 `webServer` 服务(由 `dsh-web-app` 提供),因此**只能挂 web / desktop profile**;headless profile 会一直 pending。session 目标还需要同 bundle 的 `sessionController`,缺失时返回 `session-controller-unavailable`
205
+ - 网关插件需要 `webServer` 服务(由 `dsh-web-app` 提供),因此**只能挂 web / desktop profile**;headless profile 会一直 pending。session 目标还需要 `sessionController`——它由组合**异步注册**(`webserver → web-runtime → connection → file-upload → session-controller`),所以插件是惰性取用,注册前调用会返回 `session-controller-unavailable`(稍后重试即可)
206
+ - **`sessionController.search` 可能被部署禁用**(`session-query` 索引 `openAt: "never"`)。此时按标题查找自动退回 `list`(读持久化 header + 标题投影,同样不激活 Agent),响应里的 `matchedBy` 标明走了哪条路
207
+ - **不能投递进子代理会话**:`resolveAgent` 对 subagent 路由拥有的会话返回 `session-busy: owned by subagent routing`;只能接普通会话
208
+ - `POST /api/dsh-bridge/sessions {"create": true, "cwd": "..."}` 可新建一条空白会话(探针/一次性任务用),它不会碰任何已有会话
206
209
  - **同一会话同时只允许一个在途 bridge 任务**:DSH 自己会排队,但无法可靠地把两次并发派活的回报各自归属,所以第二个会被明确拒绝(`session-busy`)而不是猜
207
210
  - **冷会话可能接不上**:DSH 激活 Agent 需要会话投影(`Agent activation requires a projected Session observation`),未激活的会话会返回 `session-not-activatable`;归档会话的回合会以 `blocked` 收口(`session-archived`)
208
211
  - **评审不支持会话目标**:评审员必须与编码会话隔离,"独立评审"才成立
209
212
  - session 目标不使用 `cwd`/`lane`/`model`/`--commit`(会话自带这些语义),传了会被判 `invalid-target`
210
213
  - 网关重启会丢失内存中的任务表(含 session 观察器),这是既有设计
214
+ - **DSH profile 的 pnpm 供应链冷却**:新发布的版本约 24 小时内会被 `minimumReleaseAge` 拒绝安装(`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`);管理器会把它写进 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 才放行。刚发版就装不上属正常,不是包的问题
215
+ - 升级已装组合包 = **卸载 → 重新安装**;卸载会把该包从 `dsh.profile.bundles` 移除,所以重装后**要确认它被重新选中**,否则插件不会挂载(表现为所有 `/api/dsh-bridge/*` 都落到网关鉴权层返回 401)
211
216
  - Codex Desktop Windows 本地市场技能注入 bug(见上,MCP 通道不受影响)
212
217
  - 动态插件环境无 `AbortSignal`,冷恢复失败时自动降级为一次性执行;宿主组合持久化版无此问题
213
218
  - ChatGPT Plus 写入型 MCP 的开放度取决于 OpenAI 产品策略
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Codex 主导的双向编码协作插件:ChatGPT 把编码任务下发给本机 DeepSeek Harness 的持久编码子代理执行,DeepSeek 在共享工作目录 D:\\Harness 中写代码并实时回传进度与汇报,Codex 读文件验收、反馈修正,直到通过。",
5
5
  "author": {
6
6
  "name": "FuJackGao"
@@ -84,5 +84,7 @@ dsh-task --cancel <taskId> --force # 取消 session 目标任务(会打断
84
84
 
85
85
  - 连接失败:DeepSeek Harness 未运行(网关应监听本机端口,默认 3080;挂在 web/desktop profile 时也可能就是 GUI 的端口)。
86
86
  - 派 session 任务返回 `session-writer-held` / `session-busy`:那条会话被别的写入方或另一个 bridge 任务占用;稍后重试,或改用 worker 目标。
87
+ - 返回 `session-busy: owned by subagent routing`:那是子代理会话,**不能作为投递目标**;只能接普通会话。
87
88
  - 返回 `session-not-activatable` / `session-archived`:该会话当前接不了(冷会话未能激活,或已归档);请用户先在客户端打开它,或解除归档。
89
+ - 某些部署禁用了搜索(会话索引 `openAt: never`):`--sessions` 会自动退回 `list` + 标题投影匹配,响应里的 `matchedBy` 会写 `list`。
88
90
  - 读不到 `--cwd` 目录:提示用户在 Codex 设置中把该目录加入可访问/可写目录。
@@ -29,6 +29,8 @@ import {
29
29
  classifyTarget,
30
30
  selectSession,
31
31
  mapControllerError,
32
+ sessionTitleOf,
33
+ looksLikeDisabledSearch,
32
34
  taskResponse,
33
35
  } from './session-target.mjs'
34
36
 
@@ -408,6 +410,44 @@ export async function apply(ctx) {
408
410
  return items.filter((it) => it !== null && typeof it === 'object' && typeof it.sessionId === 'string' && it.sessionId !== '')
409
411
  }
410
412
 
413
+ /**
414
+ * 退回路径:不打开 search 索引,直接遍历 list(读持久化 header + 投影缓存)。
415
+ * 有些部署把 session-query 索引配成 openAt: "never",此时 search 永远失败,
416
+ * 而按标题找会话仍必须可用——list 就是不激活 Agent 也能拿到标题的那条路。
417
+ */
418
+ const sessionList = async (query) => {
419
+ const controller = sessionControllerNow()
420
+ if (controller === undefined) {
421
+ throw Object.assign(new Error('sessionController service is not mounted in this profile'), { code: 'bridge/no-controller' })
422
+ }
423
+ const needle = query.toLowerCase()
424
+ const value = await controller.list({}, signalOf())
425
+ const items = value !== undefined && value !== null && Array.isArray(value.items) ? value.items : []
426
+ const out = []
427
+ for (const summary of items) {
428
+ if (summary === null || typeof summary !== 'object' || typeof summary.sessionId !== 'string') continue
429
+ const title = sessionTitleOf(summary)
430
+ if (title !== '' && title.toLowerCase().includes(needle)) {
431
+ out.push({ sessionId: summary.sessionId, snippet: title })
432
+ continue
433
+ }
434
+ if (typeof summary.cwd === 'string' && summary.cwd !== '' && summary.cwd.toLowerCase().includes(needle)) {
435
+ out.push({ sessionId: summary.sessionId, snippet: summary.cwd })
436
+ }
437
+ }
438
+ return out
439
+ }
440
+
441
+ /** 先 search,失败就退回 list;返回 {hits, matchedBy} 让调用方看得见走了哪条路。 */
442
+ const sessionFind = async (query) => {
443
+ try {
444
+ return { hits: await sessionSearch(query), matchedBy: 'search' }
445
+ } catch (e) {
446
+ if (!looksLikeDisabledSearch(e)) throw e
447
+ return { hits: await sessionList(query), matchedBy: 'list' }
448
+ }
449
+ }
450
+
411
451
  /** 只读解析:query → search 选一;sessionId → 直接用。任何不确定性都返回 reason。 */
412
452
  const resolveSessionTarget = async (target) => {
413
453
  const controller = sessionControllerNow()
@@ -415,9 +455,12 @@ export async function apply(ctx) {
415
455
  return { ok: false, reason: REASONS.CONTROLLER_UNAVAILABLE, message: 'sessionController service is not mounted in this profile', candidates: [] }
416
456
  }
417
457
  let hits = []
458
+ let matchedBy = 'sessionId'
418
459
  if (target.sessionId === undefined) {
419
460
  try {
420
- hits = await sessionSearch(target.query)
461
+ const found = await sessionFind(target.query)
462
+ hits = found.hits
463
+ matchedBy = found.matchedBy
421
464
  } catch (e) {
422
465
  return { ok: false, reason: REASONS.GATEWAY_INTERNAL, message: errText(e), candidates: [] }
423
466
  }
@@ -433,7 +476,7 @@ export async function apply(ctx) {
433
476
  if (result !== undefined && result !== null && result.error !== undefined) {
434
477
  return { ok: false, reason: mapControllerError(result.error), message: errText(result.error), candidates: picked.candidates }
435
478
  }
436
- return { ok: true, sessionId: picked.sessionId, snippet: picked.snippet, candidates: picked.candidates }
479
+ return { ok: true, sessionId: picked.sessionId, snippet: picked.snippet, candidates: picked.candidates, matchedBy }
437
480
  }
438
481
 
439
482
  const sessionPendingTask = (sessionId) => tasks.find((t) => (t.status === 'accepted' || t.status === 'running') && t.target !== undefined && t.target.kind === 'session' && t.target.sessionId === sessionId)
@@ -532,6 +575,7 @@ export async function apply(ctx) {
532
575
  task.target.sessionId = resolved.sessionId
533
576
  if (resolved.snippet !== undefined) task.target.snippet = resolved.snippet
534
577
  if (resolved.candidates.length > 0) task.candidates = resolved.candidates
578
+ if (resolved.matchedBy !== undefined) task.matchedBy = resolved.matchedBy
535
579
 
536
580
  const inFlight = sessionPendingTask(resolved.sessionId)
537
581
  if (inFlight !== undefined && inFlight.taskId !== task.taskId) {
@@ -846,6 +890,17 @@ export async function apply(ctx) {
846
890
  }
847
891
  const query = typeof parsed.query === 'string' && parsed.query.trim() !== '' ? parsed.query.trim() : undefined
848
892
  const sessionId = typeof parsed.sessionId === 'string' && parsed.sessionId.trim() !== '' ? parsed.sessionId.trim() : undefined
893
+ // create 是显式动作:为探针/一次性任务建一条空白会话,不去碰任何已有会话。
894
+ if (parsed.create === true) {
895
+ const cwd = typeof parsed.cwd === 'string' && parsed.cwd.trim() !== '' ? parsed.cwd.trim() : undefined
896
+ try {
897
+ const created = await controller.create(cwd === undefined ? {} : { cwd }, signalOf())
898
+ sendJson(res, 201, { sessionId: created !== undefined && created !== null ? created.sessionId : undefined, agentPreset: created === undefined || created === null ? undefined : created.agentPreset })
899
+ } catch (e) {
900
+ sendJson(res, 502, { error: errText(e), reason: mapControllerError(e) })
901
+ }
902
+ return
903
+ }
849
904
  if (query === undefined && sessionId === undefined) {
850
905
  sendJson(res, 400, { error: 'query or sessionId required', reason: REASONS.INVALID_TARGET })
851
906
  return
@@ -861,7 +916,9 @@ export async function apply(ctx) {
861
916
  sendJson(res, 200, { items: [{ sessionId: resolved.sessionId, ...(resolved.snippet === undefined ? {} : { snippet: resolved.snippet }), agentAvailable: true }], hasMore: false })
862
917
  return
863
918
  }
864
- const hits = await sessionSearch(query)
919
+ // search 失败就退回 list:有些部署把 session-query 索引配成 openAt "never"。
920
+ const found = await sessionFind(query)
921
+ const hits = found.hits
865
922
  const items = hits.slice(0, limit).map((h) => ({
866
923
  sessionId: h.sessionId,
867
924
  snippet: typeof h.snippet === 'string' ? h.snippet.slice(0, 200) : '',
@@ -880,7 +937,7 @@ export async function apply(ctx) {
880
937
  }
881
938
  }
882
939
  }
883
- sendJson(res, 200, { items, hasMore: hits.length > items.length, total: hits.length })
940
+ sendJson(res, 200, { items, hasMore: hits.length > items.length, total: hits.length, matchedBy: found.matchedBy })
884
941
  } catch (e) {
885
942
  sendJson(res, 502, { error: errText(e), reason: mapControllerError(e) })
886
943
  }
@@ -983,6 +1040,7 @@ export async function apply(ctx) {
983
1040
  taskId: task.taskId, status: task.status, kind: task.kind, cwd: task.cwd, lane: task.lane,
984
1041
  target: task.target, deliver: task.deliver, instruction: task.instruction.slice(0, 300),
985
1042
  result: task.result, error: task.error, reason: task.reason, fallback: task.fallback,
1043
+ ...(task.matchedBy === undefined ? {} : { matchedBy: task.matchedBy }),
986
1044
  ...(Array.isArray(task.candidates) && task.candidates.length > 0 ? { candidates: task.candidates } : {}),
987
1045
  ...(record === undefined ? {} : { watcher: record.watcher, sawRunning: record.sawRunning }),
988
1046
  })
@@ -190,6 +190,34 @@ export const mapControllerError = (error) => {
190
190
  }
191
191
  }
192
192
 
193
+ /**
194
+ * 从 SessionSummary 里取标题。
195
+ *
196
+ * 标题不在 summary 顶层:它来自投影缓存(`projections.values.title`),而且不同 DSH
197
+ * 版本可能给字符串或 `{ val }` 包装,两种都认。`list` 在**不激活 Agent、也不打开
198
+ * search 索引**的前提下读持久化 header 与投影缓存,因此在 search 被部署禁用
199
+ * (session-query 索引 openAt: "never")时,它是唯一还能按标题找会话的路径。
200
+ */
201
+ export const sessionTitleOf = (summary) => {
202
+ if (summary === null || typeof summary !== 'object') return ''
203
+ const projections = summary.projections
204
+ if (projections === null || typeof projections !== 'object') return ''
205
+ const values = projections.values
206
+ if (values === null || typeof values !== 'object') return ''
207
+ const title = values.title
208
+ if (typeof title === 'string') return title
209
+ if (title !== null && typeof title === 'object' && typeof title.val === 'string') return title.val
210
+ return ''
211
+ }
212
+
213
+ /** search 在本部署被禁用时的报错特征(session-query 索引 openAt: "never")。 */
214
+ export const looksLikeDisabledSearch = (error) => {
215
+ const text = error !== null && typeof error === 'object' && typeof error.message === 'string'
216
+ ? error.message
217
+ : String(error === undefined || error === null ? '' : error)
218
+ return /search is disabled/i.test(text)
219
+ }
220
+
193
221
  /** 任务在 HTTP 行上的回显:调用方一眼能看出这次是"新建"还是"接进哪个会话"。 */
194
222
  export const taskResponse = (task) => {
195
223
  const out = { taskId: task.taskId, status: task.status, kind: task.kind }
package/mcp/dsh-mcp.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  // 客户端(Secure MCP Tunnel / cloudflared)桥接给 ChatGPT 等远程产品。
7
7
  const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
8
8
  const SERVER_NAME = 'dsh-bridge'
9
- const SERVER_VERSION = '0.1.3'
9
+ const SERVER_VERSION = '0.1.4'
10
10
  const PROTOCOL_VERSION = '2024-11-05'
11
11
 
12
12
  const log = (...args) => console.error('[dsh-mcp]', ...args)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whaletalk/dsh-codex-collab",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Codex × DeepSeek Harness 双向编码协作:Codex 主导派活与验收,DeepSeek Harness 编码子代理在共享工作目录执行并回传,独立评审子代理实际跑通构建/测试后出具报告。提供 DSH 宿主网关插件、零依赖 MCP 服务器与 Codex 技能三件套。",
5
5
  "license": "MIT",
6
6
  "type": "module",