dsh-bettertaskgraphs 0.2.2 → 0.2.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.
package/README.md CHANGED
@@ -18,7 +18,7 @@ npm test
18
18
  npm run pack:bundle
19
19
  ```
20
20
 
21
- `build` 从前端源码生成可由 DSH Web 加载的 `client.js`。`check` 校验生成物与源码一致,`pack:bundle` 会重新构建前端并生成 `dist/dsh-bettertaskgraphs-0.2.2.tgz`。包内包含服务端模块、Web 构建产物、对应源码和构建脚本,以及 Bundle 配置补丁。
21
+ `build` 从前端源码生成可由 DSH Web 加载的 `client.js`。`check` 校验生成物与源码一致,`pack:bundle` 会重新构建前端并生成 `dist/dsh-bettertaskgraphs-0.2.3.tgz`。包内包含服务端模块、Web 构建产物、对应源码和构建脚本,以及 Bundle 配置补丁。
22
22
 
23
23
  ## 安装到独立 Web profile
24
24
 
@@ -26,7 +26,7 @@ npm run pack:bundle
26
26
 
27
27
  ```sh
28
28
  dsh --profile bettertaskgraphs --from-default-profile web --dump-config
29
- dsh plugin --profile bettertaskgraphs add "$PWD/dist/dsh-bettertaskgraphs-0.2.2.tgz"
29
+ dsh plugin --profile bettertaskgraphs add "$PWD/dist/dsh-bettertaskgraphs-0.2.3.tgz"
30
30
  dsh --profile bettertaskgraphs --dump-config
31
31
  dsh --profile bettertaskgraphs
32
32
  ```
package/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
- // Corresponding source: https://github.com/oldcitynight/dsh-bettertaskgraphs/tree/v0.2.2
2
+ // Corresponding source: https://github.com/oldcitynight/dsh-bettertaskgraphs/tree/v0.2.3
3
3
  // Generated by scripts/build-client.mjs.
4
4
  window.__ModuleLoader__.load({id:"dsh-bettertaskgraphs",factory(require){
5
5
  // SPDX-License-Identifier: GPL-3.0-only
@@ -289,7 +289,6 @@ function createClientPlugin(React, z, createRemoteContribution, graphPresentatio
289
289
  .tg-badge[data-state=failed]{background:#d452522f}
290
290
  .tg-current,.tg-conditions{font-size:12px;line-height:1.4;overflow-wrap:anywhere}.tg-current{margin-bottom:7px}.tg-current .tg-badge{margin-inline-start:6px}
291
291
  .tg-conditions{list-style:decimal;margin:4px 0 0;padding-inline-start:1.7em}.tg-conditions li+li{margin-top:4px}.tg-no-conditions{margin:4px 0 0;color:var(--tg-muted);font-size:12px}
292
- .tg-error-only{padding:6px 9px;font-size:12px}
293
292
 
294
293
  .tg-viewer{
295
294
  --tg-panel:var(--dsw-alias-bg-base,Canvas);
@@ -511,10 +510,7 @@ function createClientPlugin(React, z, createRemoteContribution, graphPresentatio
511
510
  void refreshStatus()
512
511
  return () => { controller.abort(); clearTimeout(timer) }
513
512
  }, [canRead, opened, running, sessionId, refreshCount])
514
- if (!canRead || !opened || result?.sessionId !== sessionId) return null
515
- if (!result.graph) return result.failed ? h('section', { className: 'tg-dock tg-error-only', 'aria-label': t('title') },
516
- h('style', null, styles), h('span', { role: 'status' }, t('unavailable')), ' ',
517
- h('button', { type: 'button', onClick: refresh }, t('retry'))) : null
513
+ if (!canRead || !opened || result?.sessionId !== sessionId || !result.graph) return null
518
514
  return h(GraphPanel, { key: sessionId, graph: result.graph, t, busy: result.busy, failed: result.failed, refresh })
519
515
  }
520
516
  }
package/client.source.mjs CHANGED
@@ -75,7 +75,6 @@ export function createClientPlugin(React, z, createRemoteContribution, graphPres
75
75
  .tg-badge[data-state=failed]{background:#d452522f}
76
76
  .tg-current,.tg-conditions{font-size:12px;line-height:1.4;overflow-wrap:anywhere}.tg-current{margin-bottom:7px}.tg-current .tg-badge{margin-inline-start:6px}
77
77
  .tg-conditions{list-style:decimal;margin:4px 0 0;padding-inline-start:1.7em}.tg-conditions li+li{margin-top:4px}.tg-no-conditions{margin:4px 0 0;color:var(--tg-muted);font-size:12px}
78
- .tg-error-only{padding:6px 9px;font-size:12px}
79
78
 
80
79
  .tg-viewer{
81
80
  --tg-panel:var(--dsw-alias-bg-base,Canvas);
@@ -297,10 +296,7 @@ export function createClientPlugin(React, z, createRemoteContribution, graphPres
297
296
  void refreshStatus()
298
297
  return () => { controller.abort(); clearTimeout(timer) }
299
298
  }, [canRead, opened, running, sessionId, refreshCount])
300
- if (!canRead || !opened || result?.sessionId !== sessionId) return null
301
- if (!result.graph) return result.failed ? h('section', { className: 'tg-dock tg-error-only', 'aria-label': t('title') },
302
- h('style', null, styles), h('span', { role: 'status' }, t('unavailable')), ' ',
303
- h('button', { type: 'button', onClick: refresh }, t('retry'))) : null
299
+ if (!canRead || !opened || result?.sessionId !== sessionId || !result.graph) return null
304
300
  return h(GraphPanel, { key: sessionId, graph: result.graph, t, busy: result.busy, failed: result.failed, refresh })
305
301
  }
306
302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bettertaskgraphs",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Transactional task graphs with a conversation panel for DeepSeek Harness",
5
5
  "license": "GPL-3.0-only",
6
6
  "keywords": ["dsh", "deepseek-harness", "cordis", "task-graph"],
package/projection.mjs CHANGED
@@ -12,6 +12,7 @@ const bounded = n => z.string().min(1).max(n)
12
12
  const hex = z.string().regex(/^[a-f0-9]{64}$/)
13
13
  const seq = z.number().int().nonnegative()
14
14
  const workSchema = z.object({ rootCallId: bounded(512), callSeq: seq, rootResultSeq: seq.nullable(), eventDigest: hex }).strict()
15
+ const bashFailureSchema = z.object({ rootCallId: bounded(512), callId: bounded(512) }).strict()
15
16
  const factSchema = z.object({ seq, callId: bounded(512), tool: bounded(300), isError: z.boolean(),
16
17
  outcome: z.enum(['UNVERIFIED', 'FAIL', 'NOT_RUN', 'UNKNOWN']) }).strict()
17
18
  const intentSchema = z.object({ version: z.literal(4), operationId: bounded(80), callId: bounded(512),
@@ -30,13 +31,14 @@ const pendingSchema = z.object({ intentSeq: seq, data: intentSchema,
30
31
  export const stateSchema = z.object({ sessionId: bounded(512), ownFrom: seq, turn: seq, lastSeq: z.number().int().min(-1),
31
32
  workStarted: seq, workExecuted: seq, uncertainties: z.array(workSchema).max(1024), acknowledgedWork: z.array(workSchema).max(1024),
32
33
  calls: z.record(z.string(), z.object({ tool: bounded(300), turn: seq, seq, eventDigest: hex }).strict()),
34
+ bashFailures: z.array(bashFailureSchema).max(256),
33
35
  dispatches: z.array(dispatchSchema).max(256), intents: z.array(pendingSchema).max(1024),
34
36
  proofs: z.array(proofSchema).max(MAX_PROOFS), receipts: z.array(z.object({ operationId: bounded(80), seq,
35
37
  status: z.enum(['COMMITTED', 'ABORTED']), graphRevision: seq, graphChecksum: hex.nullable(), eventDigest: hex }).strict()).max(1024), failure: z.string().nullable() }).strict()
36
38
  export function initialState(header, inheritedEventCount = 0) {
37
39
  return { sessionId: header.id, ownFrom: inheritedEventCount, turn: 0, lastSeq: -1, workStarted: 0, workExecuted: 0,
38
40
  uncertainties: [], acknowledgedWork: [],
39
- calls: {}, dispatches: [], intents: [], proofs: [], receipts: [], failure: null }
41
+ calls: {}, bashFailures: [], dispatches: [], intents: [], proofs: [], receipts: [], failure: null }
40
42
  }
41
43
  export function outcomeOfContent(content = []) {
42
44
  const outcomes = new Set()
@@ -55,6 +57,11 @@ export function outcomeOfContent(content = []) {
55
57
  // A first FAIL/NOT_RUN block may not hide a later UNKNOWN/contradiction.
56
58
  return outcomes.has('UNKNOWN') || outcomes.size > 1 ? 'UNKNOWN' : [...outcomes][0] ?? 'UNVERIFIED'
57
59
  }
60
+ function toolOutcome(state, tool, rootCallId, callId, content) {
61
+ const outcome = outcomeOfContent(content)
62
+ if (tool !== 'bash' || !state.bashFailures.some(row => row.rootCallId === rootCallId && row.callId === callId)) return outcome
63
+ return outcome === 'UNKNOWN' || outcome === 'NOT_RUN' ? 'UNKNOWN' : 'FAIL'
64
+ }
58
65
  export function chainQualification(start, root, dispatches) {
59
66
  let child = start
60
67
  const visited = new Set()
@@ -94,7 +101,7 @@ export function applyEvent(state, event) {
94
101
  if (event.seq < state.ownFrom) return state // Fork parents cannot authorize a child's graph.
95
102
  if (event.type.startsWith('plugin:project-transaction-graph/') && ![INTENT, RECEIPT, RECOVERY].includes(event.type))
96
103
  throw new GraphError('LEGACY_GRAPH_REQUIRES_EXPLICIT_MIGRATION')
97
- if (!['turn/start', 'turn/end', 'tool/call', 'tool/ptc-dispatch', 'tool/result', INTENT, RECEIPT, RECOVERY].includes(event.type)) return state
104
+ if (!['turn/start', 'turn/end', 'tool/call', 'tool/ptc-dispatch', 'tool/result', 'job/settled', INTENT, RECEIPT, RECOVERY].includes(event.type)) return state
98
105
  const d = event.data
99
106
  let next = { ...state, lastSeq: event.seq }
100
107
  if (event.type === 'turn/start') return { ...next, turn: d.turn }
@@ -126,6 +133,18 @@ export function applyEvent(state, event) {
126
133
  rootResultSeq: event.type === RECOVERY ? event.seq : null, rootResultDigest: event.type === RECOVERY ? digest(event) : null,
127
134
  workStartedAtRoot: event.type === RECOVERY ? state.workStarted : null, workExecutedAtRoot: event.type === RECOVERY ? state.workExecuted : null }] }
128
135
  }
136
+ if (event.type === 'job/settled') {
137
+ const origin = d.toolCall
138
+ if (d.kind !== 'bash' || d.owner !== state.sessionId || !origin || !Object.hasOwn(state.calls, origin.rootCallId)
139
+ || d.status !== 'completed' || d.producerReported !== true) return next
140
+ // Native Bash reports a nonzero exit in the owned job log even when its tool message is not an error.
141
+ const exit = /^exit code: (-?\d+)(?:;|$)/.exec(d.detail ?? '')
142
+ if (!exit || Number(exit[1]) === 0) return next
143
+ const failure = bashFailureSchema.parse({ rootCallId: origin.rootCallId, callId: origin.callId })
144
+ if (state.bashFailures.some(row => row.rootCallId === failure.rootCallId && row.callId === failure.callId)) return next
145
+ if (state.bashFailures.length >= 256) throw new GraphError('BASH_FAILURE_RECORD_LIMIT')
146
+ return { ...next, bashFailures: [...state.bashFailures, failure] }
147
+ }
129
148
  if (event.type === 'tool/ptc-dispatch') {
130
149
  const rootCallId = d.rootCallId ?? d.parentCallId
131
150
  if (!Object.hasOwn(state.calls, rootCallId)) {
@@ -136,7 +155,7 @@ export function applyEvent(state, event) {
136
155
  }
137
156
  const dispatch = dispatchSchema.parse({ seq: event.seq, subCallId: d.subCallId, parentCallId: d.parentCallId,
138
157
  rootCallId, tool: d.name, isError: d.isError,
139
- outcome: outcomeOfContent(d.content), eventDigest: digest(event), turn: state.turn })
158
+ outcome: toolOutcome(state, d.name, rootCallId, d.subCallId, d.content), eventDigest: digest(event), turn: state.turn })
140
159
  if (state.dispatches.some(row => row.subCallId === dispatch.subCallId && row.rootCallId === dispatch.rootCallId)
141
160
  || state.dispatches.length >= 256) throw new GraphError('PTC_DISPATCH_REPLAY_OR_LIMIT')
142
161
  return { ...next, workStarted: state.workStarted + (!internal(d.name) && d.name !== 'run_code' ? 1 : 0),
@@ -145,9 +164,6 @@ export function applyEvent(state, event) {
145
164
  if (event.type === 'tool/result') {
146
165
  const root = d.message.source.callId, success = d.message.isError !== true // Optional false in the published ToolResultMessage API.
147
166
  const syntheticUnknown = [TOOL_OUTCOME_UNKNOWN, TOOL_NOT_STARTED].includes(d.error?.code)
148
- const rootOutcome = outcomeOfContent(d.message.content)
149
- const rootQualification = syntheticUnknown || rootOutcome === 'UNKNOWN' ? 'UNKNOWN'
150
- : !success || ['FAIL', 'NOT_RUN'].includes(rootOutcome) ? 'FAILED' : 'SUCCESS'
151
167
  const sources = state.dispatches.filter(row => row.rootCallId === root)
152
168
  const call = state.calls[root]
153
169
  if (!call) {
@@ -158,6 +174,9 @@ export function applyEvent(state, event) {
158
174
  return next
159
175
  }
160
176
  const standaloneCode = call.tool === 'run_code' && !sources.length
177
+ const rootOutcome = toolOutcome(state, call.tool, root, root, d.message.content)
178
+ const rootQualification = syntheticUnknown || rootOutcome === 'UNKNOWN' ? 'UNKNOWN'
179
+ : !success || ['FAIL', 'NOT_RUN'].includes(rootOutcome) ? 'FAILED' : 'SUCCESS'
161
180
  const leafCode = sources.filter(row => row.tool === 'run_code' && !sources.some(child => child.parentCallId === row.subCallId))
162
181
  const workStarted = state.workStarted + (standaloneCode ? 1 : 0) + leafCode.length
163
182
  const executed = sources.filter(row => !internal(row.tool) && (row.tool !== 'run_code' || leafCode.includes(row)) && !['NOT_RUN', 'UNKNOWN'].includes(row.outcome)).length
@@ -189,6 +208,7 @@ export function applyEvent(state, event) {
189
208
  if (uncertainties.length > 1024) throw new GraphError('UNRESOLVED_WORK_LIMIT')
190
209
  }
191
210
  return { ...next, calls, intents, workStarted, workExecuted, uncertainties, dispatches: state.dispatches.filter(row => row.rootCallId !== root),
211
+ bashFailures: state.bashFailures.filter(row => row.rootCallId !== root),
192
212
  proofs: [...state.proofs, ...proofs].slice(-MAX_PROOFS) }
193
213
  }
194
214
  if (event.type === RECEIPT) {
@@ -202,4 +222,4 @@ export function applyEvent(state, event) {
202
222
  return next
203
223
  } catch (error) { return { ...state, failure: `Transaction projection failed at ${event.seq}: ${error.message}` } }
204
224
  }
205
- export const projection = { key: KEY, stateVersion: 5, stateSchema, init: initialState, apply: applyEvent }
225
+ export const projection = { key: KEY, stateVersion: 6, stateSchema, init: initialState, apply: applyEvent }