dsh-cindy-host 0.1.27 → 0.1.28

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,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.28 — 2026-10-28
4
+
5
+ - Fix busy input confirmation on both controllers: DSH admission no longer removes an actual pending inbox row from later projections.
6
+ - Reconcile queued, next-step steering, claimed and accepted states using real inbox/user evidence; never infer consumption from a lagging mirror.
7
+ - Preserve pendingQueue across an Android push overtaking its enqueue ACK; remove it only on genuine claim/consumption/cancellation.
8
+ - Test current Android projection parsing and authority epochs, alongside the existing durable delivery regressions.
9
+
3
10
  ## 0.1.27 — 2026-10-28
4
11
 
5
12
  - Implements current input-delivery v1 in Host: transactional complete-payload ownership before ACK, receipts, cancellation seals, authoritative clear boundaries, immutable logical identity and recoverable edits.
package/README.md CHANGED
@@ -7,12 +7,16 @@ A Host-side adapter connecting current Cindy clients to DeepSeek Harness.
7
7
  Version 0.1.27 targets the verified **DSH 0.2.1-rc.3** service contracts, with **Node.js >=22.16**. Its DSH peer versions are pinned to this carrier to prevent an installer from mixing a newer prerelease SDK into the running host. The verified Desktop runtime includes Node.js 24.21. Install through the Desktop DSH plugin manager. CLI-managed profiles can use:
8
8
 
9
9
  ```sh
10
- dsh plugin --profile <profile> add dsh-cindy-host@0.1.27
10
+ dsh plugin --profile <profile> add dsh-cindy-host@0.1.28
11
11
  ```
12
12
 
13
13
  Reload the Host plugin after installation. A full DSH restart is separate. Keep existing Cindy sign-in, device identity and settings; do not run two Host processes with the same identity.
14
14
 
15
- ## 0.1.27: current durable input delivery
15
+ ## 0.1.28: busy enqueue confirmation
16
+
17
+ A real DSH pending inbox occurrence remains in pendingQueue (or steeringQueueClientIds) in enqueue replies, pushes and polling until it is actually claimed or committed. Handing input to DSH is not delivery to the user turn. This fixes 0.1.27 empty follow-up projections overwriting the busy-queue acknowledgment on Android and desktop. Regression tests execute the current Android parser and stale-ACK authority handling, not just the durable outbox state machine.
18
+
19
+ ## Current durable input delivery
16
20
 
17
21
  - Implements input-delivery v1, not a legacy history-only fallback. Complete text and attachment bytes are held in a private, FULL-synchronous SQLite ledger **before enqueue acknowledgment**.
18
22
  - Provides per-clientId pending/accepted/removed/unknown receipts and real chronological around/around-client-id history. Accepted means a real user event was flushed and verified through public persistent storage, not an empty inbox or an in-memory event.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cindy-host",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "private": false,
5
5
  "description": "Cindy 手机客户端连接 DSH 的 Host 侧插件:会话/消息/审批/todo/goal 的手机通道,以及配套的设置卡片",
6
6
  "license": "MIT",
@@ -17,19 +17,40 @@ function identity(sessionId,clientId){if(typeof sessionId!=='string'||!sessionId
17
17
  function requestShape(row){return Object.fromEntries(['text','persistedContent','files','agentReferences','sessionRefs','origin','sourcePlugin','createOpts'].filter(k=>row[k]!==undefined).map(k=>[k,row[k]]));}
18
18
  /** One owner for durable handoff, session command serialization, and permission to retry admission. */
19
19
  export function createInputDeliveryCoordinator({store,backend,baseProjection=()=>({pendingQueue:[],steeringQueueClientIds:[]}),onChange=()=>{},onError=()=>{},validateProjection=()=>true,now=()=>Date.now()}){
20
- const lanes=new Map(), scheduled=new Set();let stopped=false;
20
+ const lanes=new Map(), scheduled=new Set(), runningBySession=new Map();let stopped=false;
21
21
  function notify(sid){queueMicrotask(()=>Promise.resolve().then(()=>onChange(sid)).catch(onError));}
22
22
  function lane(sid,fn){const previous=lanes.get(sid)??Promise.resolve();const result=previous.catch(()=>{}).then(fn);const tail=result.catch(()=>{}).finally(()=>{if(lanes.get(sid)===tail)lanes.delete(sid);});lanes.set(sid,tail);return result;}
23
23
  function save(entry){store.transaction(()=>store.put(entry));}
24
24
  function view(sid,ids=[],override){
25
- const base=override??baseProjection(sid);const pending=(base.pendingQueue??[]).filter(row=>!terminal(store.get(sid,row.clientId)?.phase));
26
- for(const e of store.list(sid))if(e.phase==='stored' && e.queued){const at=pending.findIndex(p=>p.clientId===e.clientId);if(at<0)pending.push(e.queued);else pending[at]=e.queued;}
27
- return {...base,sessionId:sid,pendingQueue:pending,inputDeliveryVersion:1,clearBoundaryMs:store.boundary(sid),deliveryReceipts:ids.map(clientId=>{const e=store.get(sid,clientId);return{clientId,state:e?e.phase==='removed'?'removed':e.phase==='accepted'?'accepted':e.payload===undefined?'unknown':'pending':'unknown'};})};
25
+ const base={...baseProjection(sid),...override};
26
+ const entries=store.list(sid);const owned=new Map(entries.map(e=>[e.clientId,e]));
27
+ const isSteering=e=>e.phase==='DSH-pending'&&e.placement==='next-step';
28
+ const pending=(base.pendingQueue??[]).filter(row=>{const e=owned.get(row.clientId);return !e||(!terminal(e.phase)&&e.phase!=='claimed'&&!isSteering(e));});
29
+ const steering=new Set((base.steeringQueueClientIds??[]).filter(cid=>{const e=owned.get(cid);return !e||isSteering(e);}));
30
+ // Admission into DSH is not consumption: keep the real pending occurrence visible
31
+ // even before the independent control-stream mirror catches up.
32
+ for(const e of entries){
33
+ if(isSteering(e)){steering.add(e.clientId);continue;}
34
+ if((e.phase==='stored'||e.phase==='DSH-pending')&&e.queued){const at=pending.findIndex(p=>p.clientId===e.clientId);if(at<0)pending.push(e.queued);else pending[at]=e.queued;}
35
+ }
36
+ const running=runningBySession.get(sid)??base.turnInProgress;
37
+ return {...base,...(typeof running==='boolean'?{turnInProgress:running}:{}),sessionId:sid,pendingQueue:pending,steeringQueueClientIds:[...steering],inputDeliveryVersion:1,clearBoundaryMs:store.boundary(sid),deliveryReceipts:ids.map(clientId=>{const e=store.get(sid,clientId);return{clientId,state:e?e.phase==='removed'?'removed':e.phase==='accepted'?'accepted':e.payload===undefined?'unknown':'pending':'unknown'};})};
28
38
  }
29
39
  async function evidence(sid){
30
40
  const proof=await backend.evidence(sid);
31
41
  if(!proof || !Array.isArray(proof.users) || !Array.isArray(proof.inbox))throw deliveryError('DELIVERY_EVIDENCE_UNAVAILABLE','Durable DSH evidence is unavailable');
32
- store.transaction(()=>{for(const user of proof.users){const e=store.get(sid,user.clientId);if(e?.phase==='removed')throw deliveryError('DELIVERY_CANCEL_CONFLICT','A sealed cancellation has a durable user message');if(e && e.phase!=='accepted')store.put({...e,phase:'accepted',payload:null,userSeq:user.seq,error:null});}});
42
+ let changed=false;const held=new Map(proof.inbox.map(item=>[item.clientId,item]));
43
+ store.transaction(()=>{
44
+ for(const user of proof.users){const e=store.get(sid,user.clientId);if(e?.phase==='removed')throw deliveryError('DELIVERY_CANCEL_CONFLICT','A sealed cancellation has a durable user message');if(e&&e.phase!=='accepted'){store.put({...e,phase:'accepted',payload:null,userSeq:user.seq,error:null});changed=true;}}
45
+ for(const e of store.list(sid)){
46
+ if(terminal(e.phase)||e.phase==='stored')continue;
47
+ const item=held.get(e.clientId);
48
+ if(item){const placement=item.placement??'next-turn';if(e.phase!=='DSH-pending'||e.placement!==placement){store.put({...e,phase:'DSH-pending',placement});changed=true;}}
49
+ else if(e.phase==='DSH-pending'){store.put({...e,phase:'claimed',placement:null});changed=true;}
50
+ }
51
+ });
52
+ if(typeof proof.running==='boolean'){if(runningBySession.get(sid)!==proof.running)changed=true;runningBySession.set(sid,proof.running);}
53
+ if(changed)notify(sid);
33
54
  return proof;
34
55
  }
35
56
  async function runSession(sid){
@@ -4,4 +4,4 @@
4
4
  */
5
5
  export const HOST_APP_VERSION = '0.1.82';
6
6
  /** Actual npm release, separate from the Cindy compatibility identity. */
7
- export const HOST_PLUGIN_VERSION = '0.1.27';
7
+ export const HOST_PLUGIN_VERSION = '0.1.28';