@whaletalk/dsh-codex-collab 0.1.2 → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Codex 主导的双向编码协作插件:ChatGPT 把编码任务下发给本机 DeepSeek Harness 的持久编码子代理执行,DeepSeek 在共享工作目录 D:\\Harness 中写代码并实时回传进度与汇报,Codex 读文件验收、反馈修正,直到通过。",
5
5
  "author": {
6
6
  "name": "FuJackGao"
@@ -46,9 +46,11 @@ export async function apply(ctx) {
46
46
  const sessionPersistence = ctx.get('sessionPersistence')
47
47
  const workspaceRegistry = ctx.get('workspaceRegistry')
48
48
  const llm = ctx.get('llm')
49
- // 已有会话的读写入口(由 dsh-web-app bundle 提供,与 webServer 同源)。
50
- // 不放进 inject:缺失时降级为稳定的 session-controller-unavailable,而不是整个插件挂掉。
51
- const sessionController = ctx.get('sessionController')
49
+ // 已有会话的读写入口。**必须惰性取**:@deepseek-ai/dsh-api-session-controller 的
50
+ // sessionController 由组合异步注册(DSH 启动日志里它先处于 waiting for services),
51
+ // 在 apply() 时捕获会永久拿到 undefined。每次使用时现取;缺失时返回稳定的
52
+ // session-controller-unavailable,而不是让整个插件挂掉。
53
+ const sessionControllerNow = () => ctx.get('sessionController')
52
54
 
53
55
  // 清理历史版本可能泄漏的路由条目(重启前的动态插件、旧版本次插件)
54
56
  try {
@@ -397,14 +399,19 @@ export async function apply(ctx) {
397
399
  const sessionTargets = new Map()
398
400
 
399
401
  const sessionSearch = async (query) => {
400
- const value = await sessionController.search({ query }, signalOf())
402
+ const controller = sessionControllerNow()
403
+ if (controller === undefined) {
404
+ throw Object.assign(new Error('sessionController service is not mounted in this profile'), { code: 'bridge/no-controller' })
405
+ }
406
+ const value = await controller.search({ query }, signalOf())
401
407
  const items = value !== undefined && value !== null && Array.isArray(value.items) ? value.items : []
402
408
  return items.filter((it) => it !== null && typeof it === 'object' && typeof it.sessionId === 'string' && it.sessionId !== '')
403
409
  }
404
410
 
405
411
  /** 只读解析:query → search 选一;sessionId → 直接用。任何不确定性都返回 reason。 */
406
412
  const resolveSessionTarget = async (target) => {
407
- if (sessionController === undefined) {
413
+ const controller = sessionControllerNow()
414
+ if (controller === undefined) {
408
415
  return { ok: false, reason: REASONS.CONTROLLER_UNAVAILABLE, message: 'sessionController service is not mounted in this profile', candidates: [] }
409
416
  }
410
417
  let hits = []
@@ -419,7 +426,7 @@ export async function apply(ctx) {
419
426
  if (!picked.ok) return picked
420
427
  let result
421
428
  try {
422
- result = await sessionController.resolveAgent(picked.sessionId)
429
+ result = await controller.resolveAgent(picked.sessionId)
423
430
  } catch (e) {
424
431
  return { ok: false, reason: mapControllerError(e), message: errText(e), candidates: picked.candidates }
425
432
  }
@@ -478,7 +485,9 @@ export async function apply(ctx) {
478
485
  record.watcher = 'follow'
479
486
  void (async () => {
480
487
  try {
481
- const stream = sessionController.follow({ address: { kind: 'session', sessionId: record.sessionId } }, controller.signal)
488
+ const controllerRef = sessionControllerNow()
489
+ if (controllerRef === undefined) throw new Error('sessionController is not mounted')
490
+ const stream = controllerRef.follow({ address: { kind: 'session', sessionId: record.sessionId } }, controller.signal)
482
491
  for await (const frame of stream) {
483
492
  if (record.settled) break
484
493
  if (frame === null || typeof frame !== 'object') continue
@@ -552,7 +561,17 @@ export async function apply(ctx) {
552
561
  sessionTargets.set(resolved.sessionId, record)
553
562
 
554
563
  try {
555
- await sessionController.prompt({
564
+ const controllerRef = sessionControllerNow()
565
+ if (controllerRef === undefined) {
566
+ sessionTargets.delete(resolved.sessionId)
567
+ record.settled = true
568
+ task.status = 'error'
569
+ task.error = 'sessionController service is not mounted in this profile'
570
+ task.reason = REASONS.CONTROLLER_UNAVAILABLE
571
+ pushTask(task.taskId, 'error', { error: task.error, reason: task.reason, target: { kind: 'session', sessionId: resolved.sessionId } })
572
+ return { task, failed: { reason: task.reason, message: task.error, candidates: [] } }
573
+ }
574
+ await controllerRef.prompt({
556
575
  requestId: task.taskId,
557
576
  sessionId: resolved.sessionId,
558
577
  mode: deliver,
@@ -720,7 +739,8 @@ export async function apply(ctx) {
720
739
  }
721
740
 
722
741
  // 完成信号:会话从 running 回到 idle 即这一轮结束(follow 只负责流式进度)。
723
- if (sessionController !== undefined) {
742
+ // 订阅的是 ctx 上的事件,与服务何时注册无关,所以不设门禁。
743
+ {
724
744
  ctx.effect(() => {
725
745
  const off = ctx.on('api-session/status', (sessionId, running) => {
726
746
  const record = sessionTargets.get(sessionId)
@@ -819,8 +839,9 @@ export async function apply(ctx) {
819
839
  } else {
820
840
  parsed = qsOf(req)
821
841
  }
822
- if (sessionController === undefined) {
823
- sendJson(res, 503, { error: 'sessionController service is not mounted in this profile', reason: REASONS.CONTROLLER_UNAVAILABLE })
842
+ const controller = sessionControllerNow()
843
+ if (controller === undefined) {
844
+ sendJson(res, 503, { error: 'sessionController service is not mounted yet (it registers asynchronously after startup; retry shortly)', reason: REASONS.CONTROLLER_UNAVAILABLE })
824
845
  return
825
846
  }
826
847
  const query = typeof parsed.query === 'string' && parsed.query.trim() !== '' ? parsed.query.trim() : undefined
@@ -849,7 +870,7 @@ export async function apply(ctx) {
849
870
  // 只对前 5 条探能力:逐条 stat 会很贵,这里保持有界。
850
871
  for (const item of items.slice(0, 5)) {
851
872
  try {
852
- const r = await sessionController.resolveAgent(item.sessionId)
873
+ const r = await controller.resolveAgent(item.sessionId)
853
874
  const failed = r !== undefined && r !== null && r.error !== undefined
854
875
  item.agentAvailable = !failed
855
876
  if (failed) item.problem = mapControllerError(r.error)
@@ -914,7 +935,9 @@ export async function apply(ctx) {
914
935
  }
915
936
  let cancelled = false
916
937
  try {
917
- await sessionController.cancel({ sessionId: task.target.sessionId }, signalOf())
938
+ const controller = sessionControllerNow()
939
+ if (controller === undefined) throw new Error('sessionController service is not mounted in this profile')
940
+ await controller.cancel({ sessionId: task.target.sessionId }, signalOf())
918
941
  cancelled = true
919
942
  } catch (e) {
920
943
  console.error('[dsh-bridge] session cancel failed:', errText(e).slice(0, 200))
@@ -971,7 +994,7 @@ export async function apply(ctx) {
971
994
  lanes: Array.from(buckets.keys()),
972
995
  reviewers: Array.from(reviewerBuckets.keys()),
973
996
  sessions: Array.from(sessionTargets.keys()),
974
- sessionController: sessionController !== undefined,
997
+ sessionController: sessionControllerNow() !== undefined,
975
998
  tasks: tasks.slice(-20).map((t) => ({ taskId: t.taskId, kind: t.kind, status: t.status, cwd: t.cwd, lane: t.lane, target: t.target, deliver: t.deliver })),
976
999
  })
977
1000
  },
@@ -982,7 +1005,15 @@ export async function apply(ctx) {
982
1005
  path: '/api/dsh-bridge/debug',
983
1006
  handler: async (req, res) => {
984
1007
  if (!isLoopback(req)) { res.writeHead(403); res.end('forbidden'); return }
985
- const out = { pending: 0, historyCount: history.length, presets: [], providers: [], buckets: [], reviewers: [], sessionController: sessionController !== undefined, sessionTargets: [], models: knownModels, modelProvider, modelListError, workspaces: [] }
1008
+ // services 是现场探测(不是 apply 时的快照):sessionController 可能晚于本插件注册,
1009
+ // 这份清单能在"接不上会话"时直接告诉我们到底缺哪个服务。
1010
+ const probe = (name) => { try { return ctx.get(name) !== undefined } catch (e) { return false } }
1011
+ const out = {
1012
+ pending: 0, historyCount: history.length, presets: [], providers: [], buckets: [], reviewers: [],
1013
+ sessionController: probe('sessionController'),
1014
+ services: Object.fromEntries(['sessionController', 'connection', 'sessionQuery', 'sessionPersistence', 'workspaceRegistry', 'agents', 'subagents', 'agentPresets', 'timer', 'llm'].map((n) => [n, probe(n)])),
1015
+ sessionTargets: [], models: knownModels, modelProvider, modelListError, workspaces: [],
1016
+ }
986
1017
  out.pending = tasks.filter((t) => t.status === 'accepted' || t.status === 'running').length
987
1018
  try { out.providers = subagents.list() } catch (e) { out.providers = ['list failed: ' + errText(e)] }
988
1019
  if (agentPresets !== undefined) {
@@ -1124,5 +1155,5 @@ export async function apply(ctx) {
1124
1155
  sessionTargets.clear()
1125
1156
  })
1126
1157
 
1127
- console.log('[dsh-bridge] persistent gateway ready (host composition): task(new|session) · sessions · review · tasks · cancel · status · debug · WS | sessionController=' + (sessionController !== undefined ? 'yes' : 'no'))
1158
+ console.log('[dsh-bridge] persistent gateway ready (host composition): task(new|session) · sessions · review · tasks · cancel · status · debug · WS | sessionController=' + (sessionControllerNow() !== undefined ? 'yes' : 'late-bound (registers asynchronously)'))
1128
1159
  }
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.2'
9
+ const SERVER_VERSION = '0.1.3'
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.2",
3
+ "version": "0.1.3",
4
4
  "description": "Codex × DeepSeek Harness 双向编码协作:Codex 主导派活与验收,DeepSeek Harness 编码子代理在共享工作目录执行并回传,独立评审子代理实际跑通构建/测试后出具报告。提供 DSH 宿主网关插件、零依赖 MCP 服务器与 Codex 技能三件套。",
5
5
  "license": "MIT",
6
6
  "type": "module",