dsh-plugin-soulbridge 0.0.111 → 0.0.113
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 +15 -0
- package/index.js +219 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ dsh plugin --profile desktop add "github:Jason0826/dsh-plugin-soulbridge#v0.0.4"
|
|
|
45
45
|
| `user/message source=user chars=N` | 收到用户消息 |
|
|
46
46
|
| `soul ready: db=… soul=… user=…` | core 与数据库就绪 |
|
|
47
47
|
| `observe ok: chars=… written=… affect=… confidence=…` | **整条链路成功** |
|
|
48
|
+
| `turn cost: session=… turn=… requests=N (steps=… failed=… plugin=…)` | 这一轮的请求数到了 30(见下节) |
|
|
48
49
|
| `skipping profile bundle …` (stderr) | bundle 被启动器跳过,原因就在后面 |
|
|
49
50
|
|
|
50
51
|
## 卸载
|
|
@@ -94,6 +95,20 @@ DSH 启动器对 `package.json` 直接 `JSON.parse`,不剥 BOM。用 Windows P
|
|
|
94
95
|
为什么是"规则优先、模型补漏":实测规则精确率 1.00,但 26 条真实消息里漏了 19 条;
|
|
95
96
|
让模型覆盖规则没有收益,只在漏掉时补才有。宿主模型**只能在真实轮次内调用**,
|
|
96
97
|
启动时调用会被账号 provider 拒绝(见 `docs/experiment-2026-10-29-host-llm.md`)。
|
|
98
|
+
|
|
99
|
+
## 每轮花了多少(0026)
|
|
100
|
+
|
|
101
|
+
平台导出只有"每天一行",单轮花大钱看不出来。所以插件自己记:
|
|
102
|
+
|
|
103
|
+
| 落点 | 什么时候 | 内容 |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `trace.jsonl` 的 `{"event":"turn-cost"}` | 每轮结束 | 请求数(= 步骤 + 失败尝试 + 插件自己的调用)、四类 token、耗时、结束原因 |
|
|
106
|
+
| `plugin.log` 的 `turn cost:` | 一轮 ≥ 30 次请求 | 同一份数字压成一行 |
|
|
107
|
+
|
|
108
|
+
计数来源是宿主的事件流:`step/start`(一次调用)、`assistant/attempt`(重试且没出消息)、
|
|
109
|
+
`assistant/message.usage`(token)。子代理跑在自己的会话里,因此各自一条记录——事件流
|
|
110
|
+
里没有父子关系,猜一个比写两条更坏。插件**不算钱**:金额只从 `scripts/billing.mjs`
|
|
111
|
+
(读平台导出)来,价目有峰谷两档且会变。
|
|
97
112
|
## 升级提示:新增工具需要新开会话
|
|
98
113
|
|
|
99
114
|
`ctx.inject(['tools'])` 注册的工具在**会话创建时**进入该会话的工具集。插件升级后
|
package/index.js
CHANGED
|
@@ -88,6 +88,151 @@ function trace(record) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** A turn that reaches this many model requests gets one line in plugin.log (0026). */
|
|
92
|
+
const TURN_REQUEST_ALERT = 30;
|
|
93
|
+
/** Subagent sessions each open a turn; past this many, the oldest unfinished turn is written out. */
|
|
94
|
+
const TURN_TRACKER_MAX = 64;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* What one turn cost, in requests and tokens (brief 0026).
|
|
98
|
+
*
|
|
99
|
+
* The platform export is one line per day, so the turn that actually burned the
|
|
100
|
+
* money is invisible until the day is over. The host's event stream already
|
|
101
|
+
* carries everything needed to attribute it:
|
|
102
|
+
*
|
|
103
|
+
* step/start one model call was scheduled
|
|
104
|
+
* assistant/attempt a call that was retried and produced no message
|
|
105
|
+
* assistant/message a finished call, with the usage it reported
|
|
106
|
+
* turn/start, turn/end the window
|
|
107
|
+
*
|
|
108
|
+
* A subagent runs in its own session and therefore gets its own record; the
|
|
109
|
+
* event stream has no parent link, and guessing one would be worse than two
|
|
110
|
+
* honest lines. Money is deliberately absent — prices have peak/off-peak tiers
|
|
111
|
+
* and change, so amounts come only from `scripts/billing.mjs` in core.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} [options]
|
|
114
|
+
* @param {number} [options.threshold] request count that triggers the log line.
|
|
115
|
+
* @param {number} [options.maxTurns] unfinished turns kept before the oldest is written out.
|
|
116
|
+
* @param {(record: object) => void} [options.onReport] called once per finished turn.
|
|
117
|
+
* @param {(line: string) => void} [options.onAlert] called when a turn reaches the threshold.
|
|
118
|
+
*/
|
|
119
|
+
export function createTurnCostTracker({ threshold = TURN_REQUEST_ALERT, maxTurns = TURN_TRACKER_MAX, onReport, onAlert } = {}) {
|
|
120
|
+
const open = new Map();
|
|
121
|
+
let lastSession;
|
|
122
|
+
|
|
123
|
+
const addUsage = (entry, usage) => {
|
|
124
|
+
if (!usage) return;
|
|
125
|
+
entry.input += Number(usage.inputTokens) || 0;
|
|
126
|
+
entry.cacheHit += Number(usage.cacheReadTokens) || 0;
|
|
127
|
+
entry.cacheWrite += Number(usage.cacheWriteTokens) || 0;
|
|
128
|
+
entry.output += Number(usage.outputTokens) || 0;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const requestsOf = (entry) => entry.steps + entry.failed + entry.plugin;
|
|
132
|
+
|
|
133
|
+
/** Write the record once, then forget the turn. `complete` is false for interrupted/evicted turns. */
|
|
134
|
+
const finish = (session, entry, reason, endedAt, complete) => {
|
|
135
|
+
open.delete(session);
|
|
136
|
+
const requests = requestsOf(entry);
|
|
137
|
+
if (requests === 0) return;
|
|
138
|
+
const secs = Math.max(0, Math.round((endedAt - entry.startedAt) / 1000));
|
|
139
|
+
const record = {
|
|
140
|
+
event: 'turn-cost',
|
|
141
|
+
session,
|
|
142
|
+
turn: entry.turn,
|
|
143
|
+
requests,
|
|
144
|
+
steps: entry.steps,
|
|
145
|
+
failed: entry.failed,
|
|
146
|
+
plugin: entry.plugin,
|
|
147
|
+
input: entry.input,
|
|
148
|
+
cacheHit: entry.cacheHit,
|
|
149
|
+
cacheWrite: entry.cacheWrite,
|
|
150
|
+
output: entry.output,
|
|
151
|
+
secs,
|
|
152
|
+
...(complete ? {} : { open: true }),
|
|
153
|
+
...(reason === undefined ? {} : { reason }),
|
|
154
|
+
};
|
|
155
|
+
onReport?.(record);
|
|
156
|
+
if (requests >= threshold) {
|
|
157
|
+
onAlert?.(
|
|
158
|
+
`turn cost: session=${session} turn=${entry.turn} requests=${requests}` +
|
|
159
|
+
` (steps=${entry.steps} failed=${entry.failed} plugin=${entry.plugin})` +
|
|
160
|
+
` input=${entry.input} cacheHit=${entry.cacheHit} output=${entry.output}` +
|
|
161
|
+
` secs=${secs}${complete ? '' : ' open'}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
/**
|
|
168
|
+
* Feed one session event. Events of a session whose turn was never opened are
|
|
169
|
+
* ignored rather than attached to a neighbour: a wrong attribution is worse
|
|
170
|
+
* than a missing one.
|
|
171
|
+
* @param {string} session - session id the event belongs to.
|
|
172
|
+
* @param {{type?: string, time?: number, data?: object}} event - host session event.
|
|
173
|
+
*/
|
|
174
|
+
observe(session, event) {
|
|
175
|
+
const type = event?.type;
|
|
176
|
+
const data = event?.data ?? {};
|
|
177
|
+
if (typeof data.turn !== 'number') return;
|
|
178
|
+
lastSession = session;
|
|
179
|
+
const entry = open.get(session);
|
|
180
|
+
if (type === 'turn/start') {
|
|
181
|
+
if (entry) finish(session, entry, 'restarted', event?.time ?? Date.now(), false);
|
|
182
|
+
open.set(session, {
|
|
183
|
+
turn: data.turn,
|
|
184
|
+
startedAt: event?.time ?? Date.now(),
|
|
185
|
+
steps: 0,
|
|
186
|
+
failed: 0,
|
|
187
|
+
plugin: 0,
|
|
188
|
+
input: 0,
|
|
189
|
+
cacheHit: 0,
|
|
190
|
+
cacheWrite: 0,
|
|
191
|
+
output: 0,
|
|
192
|
+
});
|
|
193
|
+
if (open.size > maxTurns) {
|
|
194
|
+
const oldest = open.keys().next().value;
|
|
195
|
+
finish(oldest, open.get(oldest), 'evicted', Date.now(), false);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!entry || entry.turn !== data.turn) return;
|
|
200
|
+
if (type === 'step/start') entry.steps += 1;
|
|
201
|
+
else if (type === 'assistant/attempt') entry.failed += 1;
|
|
202
|
+
else if (type === 'assistant/message') addUsage(entry, data.usage);
|
|
203
|
+
else if (type === 'turn/end') {
|
|
204
|
+
finish(session, entry, data.reason?.kind, event?.time ?? Date.now(), true);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* Count one model call made by this plugin, attributed to the turn it ran
|
|
209
|
+
* inside. The turn is the most recent one the host reported an event for —
|
|
210
|
+
* the stage chain only runs while that turn is live.
|
|
211
|
+
*/
|
|
212
|
+
pluginCall() {
|
|
213
|
+
if (lastSession === undefined) return;
|
|
214
|
+
const entry = open.get(lastSession);
|
|
215
|
+
if (!entry) return;
|
|
216
|
+
entry.plugin += 1;
|
|
217
|
+
},
|
|
218
|
+
/**
|
|
219
|
+
* Add the tokens a plugin call reported to the same turn. Separate from
|
|
220
|
+
* {@link pluginCall} because the request is counted when it is dispatched
|
|
221
|
+
* (a failed call still costs money) while its usage only arrives at the end.
|
|
222
|
+
* @param {{inputTokens?: number, cacheReadTokens?: number, cacheWriteTokens?: number, outputTokens?: number}} [usage]
|
|
223
|
+
* usage the call reported; absent when the call failed before reporting.
|
|
224
|
+
*/
|
|
225
|
+
pluginUsage(usage) {
|
|
226
|
+
if (lastSession === undefined) return;
|
|
227
|
+
const entry = open.get(lastSession);
|
|
228
|
+
if (!entry) return;
|
|
229
|
+
addUsage(entry, usage);
|
|
230
|
+
},
|
|
231
|
+
/** Unfinished turns currently held (observability for the bound in tests). */
|
|
232
|
+
openCount: () => open.size,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
91
236
|
/** Prompt assembly can run several times per step; do not hit SQLite every time. */
|
|
92
237
|
|
|
93
238
|
/**
|
|
@@ -134,6 +279,8 @@ function readConfig() {
|
|
|
134
279
|
/** Adapter side of the core LlmFn port: the host model, called inside a turn. */
|
|
135
280
|
let currentTurn;
|
|
136
281
|
let currentStep;
|
|
282
|
+
/** Per-turn cost accounting (0026); null until apply() attaches it (tests import this module bare). */
|
|
283
|
+
let turnCostTracker;
|
|
137
284
|
|
|
138
285
|
// Measured, not guessed: 279 calls, p50=1.7s p90=6.8s p99=46.7s, max=142.9s.
|
|
139
286
|
// 90s is ~2x p99 and caps a hung call without cutting the ordinary tail.
|
|
@@ -170,6 +317,11 @@ export function makeLlm(ctx, config) {
|
|
|
170
317
|
// a call actually lands in.
|
|
171
318
|
const started = Date.now();
|
|
172
319
|
log(`llm probe: start turn=${currentTurn ?? '?'} step=${currentStep ?? '?'} chars=${prompt.length}`);
|
|
320
|
+
// This call is a model request too (0026): count it, and keep the usage the
|
|
321
|
+
// stream reports so the turn's token total is complete rather than just its
|
|
322
|
+
// agent-loop part.
|
|
323
|
+
let usage;
|
|
324
|
+
turnCostTracker?.pluginCall();
|
|
173
325
|
try {
|
|
174
326
|
const text = await withTimeout(
|
|
175
327
|
collectText(
|
|
@@ -185,10 +337,14 @@ export function makeLlm(ctx, config) {
|
|
|
185
337
|
},
|
|
186
338
|
],
|
|
187
339
|
}),
|
|
340
|
+
(reported) => {
|
|
341
|
+
usage = reported;
|
|
342
|
+
},
|
|
188
343
|
),
|
|
189
344
|
LLM_TIMEOUT_MS,
|
|
190
345
|
'llm call',
|
|
191
346
|
);
|
|
347
|
+
turnCostTracker?.pluginUsage(usage);
|
|
192
348
|
log(
|
|
193
349
|
`llm probe: end ok ms=${Date.now() - started} out=${typeof text === 'string' ? text.length : -1}` +
|
|
194
350
|
` step=${currentStep ?? '?'}`,
|
|
@@ -215,8 +371,12 @@ export function makeLlm(ctx, config) {
|
|
|
215
371
|
* `finish{reason:{kind:'error'|'aborted'}}` instead of throwing, so without this
|
|
216
372
|
* check the failure came back as `''` and the caller read it as "the model said
|
|
217
373
|
* nothing". `collectText` throws unless some text was produced before the failure.
|
|
374
|
+
*
|
|
375
|
+
* @param {AsyncIterable<object>} stream - chunks of one model call.
|
|
376
|
+
* @param {(usage: object) => void} [onUsage] called with the usage chunk, if the
|
|
377
|
+
* adapter reported one; without it a call's tokens have no reader (0026).
|
|
218
378
|
*/
|
|
219
|
-
export async function collectText(stream) {
|
|
379
|
+
export async function collectText(stream, onUsage) {
|
|
220
380
|
let text = '';
|
|
221
381
|
let sawDelta = false;
|
|
222
382
|
let failure;
|
|
@@ -226,6 +386,8 @@ export async function collectText(stream) {
|
|
|
226
386
|
sawDelta = true;
|
|
227
387
|
} else if (!sawDelta && chunk.type === 'block-end' && chunk.block?.type === 'text') {
|
|
228
388
|
text += chunk.block.text ?? '';
|
|
389
|
+
} else if (chunk.type === 'usage' && chunk.usage) {
|
|
390
|
+
onUsage?.(chunk.usage);
|
|
229
391
|
} else if (chunk.type === 'finish' && (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted')) {
|
|
230
392
|
failure = chunk.reason;
|
|
231
393
|
}
|
|
@@ -1098,6 +1260,17 @@ function bumpExtractAttempt(turnId) {
|
|
|
1098
1260
|
);
|
|
1099
1261
|
}
|
|
1100
1262
|
|
|
1263
|
+
/**
|
|
1264
|
+
* After a thread was extracted, every queued turn of that thread is already covered.
|
|
1265
|
+
* Leaving them would extract the same topic again, turn by turn (0025).
|
|
1266
|
+
*/
|
|
1267
|
+
function removeExtractItemsOfThread(threadId, turnIds) {
|
|
1268
|
+
if (!threadId) return;
|
|
1269
|
+
writeExtractQueue(
|
|
1270
|
+
readExtractQueue().filter((item) => !turnIds.includes(item.turnId)),
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1101
1274
|
/**
|
|
1102
1275
|
* Add one turn to the extraction queue.
|
|
1103
1276
|
*
|
|
@@ -1131,7 +1304,7 @@ function hasSubstance(text) {
|
|
|
1131
1304
|
return !/^(好的?|嗯+|哦+|可以|行|ok|okay|收到|明白|继续|好)[。!!.\s]*$/i.test(trimmed);
|
|
1132
1305
|
}
|
|
1133
1306
|
|
|
1134
|
-
async function runExtraction(text, turnId, turnNo, origin = 'live') {
|
|
1307
|
+
async function runExtraction(text, turnId, turnNo, origin = 'live', thread = null) {
|
|
1135
1308
|
const config = readConfig();
|
|
1136
1309
|
if (config.extract === 'off') return 'skipped-off';
|
|
1137
1310
|
if (typeof text !== 'string' || !hasSubstance(text)) {
|
|
@@ -1153,6 +1326,10 @@ async function runExtraction(text, turnId, turnNo, origin = 'live') {
|
|
|
1153
1326
|
const previousThreadId = soul.threadBefore(turnRow.sessionId, turnNo);
|
|
1154
1327
|
if (previousThreadId) context.previousThreadId = previousThreadId;
|
|
1155
1328
|
}
|
|
1329
|
+
// 0025: the unit is the whole topic when the turn belongs to one. The turns array is
|
|
1330
|
+
// numbered the way the prompt numbers them, and each fact may name its own turn.
|
|
1331
|
+
const turns = thread?.turns ?? null;
|
|
1332
|
+
const turnIds = turns ? Object.fromEntries(turns.map((entry) => [entry.turnNo, entry.id])) : undefined;
|
|
1156
1333
|
const draft = await soul.extract(text, {
|
|
1157
1334
|
llm: useLlm ? injectedLlm : undefined,
|
|
1158
1335
|
mode: useLlm ? 'auto' : 'rules',
|
|
@@ -1162,8 +1339,9 @@ async function runExtraction(text, turnId, turnNo, origin = 'live') {
|
|
|
1162
1339
|
// The exchange, not the message (ADR 0035): my reply is where promises and
|
|
1163
1340
|
// decisions live, and it is the only way "我的承诺" can ever be extracted.
|
|
1164
1341
|
assistantText: turnRow?.assistantText ?? undefined,
|
|
1342
|
+
turns: turns ?? undefined,
|
|
1165
1343
|
});
|
|
1166
|
-
const result = soul.commitDrafts(draft, { turnId });
|
|
1344
|
+
const result = soul.commitDrafts(draft, { turnId, turnIds });
|
|
1167
1345
|
// Attribution is written even when nothing was extracted: a turn that says
|
|
1168
1346
|
// "无" (nothing worth remembering) can still be part of a thread.
|
|
1169
1347
|
const attribution = soul.applyThread(draft.thread, {
|
|
@@ -1183,6 +1361,7 @@ async function runExtraction(text, turnId, turnNo, origin = 'live') {
|
|
|
1183
1361
|
` gaps=${result.gaps.length} skipped=${result.skipped.length}` +
|
|
1184
1362
|
` slots->text=${result.droppedSlots.length} holder->world=${result.holderFallbacks.length}` +
|
|
1185
1363
|
` inferred=${inferred}` +
|
|
1364
|
+
` unit=${turns ? `${turns.length}turns` : '1turn'} threadId=${thread?.id ?? '-'}` +
|
|
1186
1365
|
` ms=${ms} turn=${turnNo} origin=${origin}` +
|
|
1187
1366
|
` thread=${attribution.threadId}${attribution.created ? '(new)' : ''}` +
|
|
1188
1367
|
` join=${attribution.joinReason} threadLine=${draft.threadLine ?? 'rules'}` +
|
|
@@ -1276,9 +1455,21 @@ export async function drainExtractionQueue() {
|
|
|
1276
1455
|
try {
|
|
1277
1456
|
const item = pickExtractionItem(queue);
|
|
1278
1457
|
let text = '';
|
|
1458
|
+
let thread = null;
|
|
1279
1459
|
try {
|
|
1280
|
-
const
|
|
1460
|
+
const soul = ensureSoul();
|
|
1461
|
+
const turn = soul.turn(item.turnId);
|
|
1281
1462
|
text = turn?.userText ?? '';
|
|
1463
|
+
// A turn is a trigger, the topic is the unit (0025). When the turn is already in a
|
|
1464
|
+
// thread, extract that thread's turns instead of this message alone.
|
|
1465
|
+
const threadId = soul.turnThreadId(item.turnId);
|
|
1466
|
+
if (threadId) {
|
|
1467
|
+
const turns = soul.threadTurns(threadId);
|
|
1468
|
+
if (turns.length > 0) {
|
|
1469
|
+
thread = { id: threadId, turns };
|
|
1470
|
+
text = turns[turns.length - 1].userText;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1282
1473
|
} catch {
|
|
1283
1474
|
text = '';
|
|
1284
1475
|
}
|
|
@@ -1288,14 +1479,21 @@ export async function drainExtractionQueue() {
|
|
|
1288
1479
|
return;
|
|
1289
1480
|
}
|
|
1290
1481
|
|
|
1291
|
-
const outcome = await runExtraction(text, item.turnId, item.turnNo, item.origin ?? 'live');
|
|
1482
|
+
const outcome = await runExtraction(text, item.turnId, item.turnNo, item.origin ?? 'live', thread);
|
|
1292
1483
|
const ok =
|
|
1293
1484
|
outcome === 'ok' || outcome === 'skipped-off' || outcome === 'skipped-empty' || outcome === 'rules-mode';
|
|
1294
1485
|
if (ok) {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1486
|
+
if (thread) {
|
|
1487
|
+
removeExtractItemsOfThread(thread.id, thread.turns.map((entry) => entry.id));
|
|
1488
|
+
// Promotion is decided after the facts exist; a whole thread can hold several
|
|
1489
|
+
// corrected turns, so every turn is offered.
|
|
1490
|
+
for (const entry of thread.turns) feedClassifyFromTurn(entry.id);
|
|
1491
|
+
} else {
|
|
1492
|
+
removeExtractItem(item.turnId);
|
|
1493
|
+
// Promotion is decided after the facts exist: a corrected turn's lessons enter
|
|
1494
|
+
// the classification queue here, which is the only live producer it has.
|
|
1495
|
+
feedClassifyFromTurn(item.turnId);
|
|
1496
|
+
}
|
|
1299
1497
|
trace({ event: 'extract-queue', drained: 1, remaining: readExtractQueue().length, outcome });
|
|
1300
1498
|
return true;
|
|
1301
1499
|
}
|
|
@@ -2244,6 +2442,14 @@ export function apply(ctx) {
|
|
|
2244
2442
|
// Backfill is triggered by the first user message, not here: at apply() time
|
|
2245
2443
|
// the host model service is not mounted yet and every call fails.
|
|
2246
2444
|
|
|
2445
|
+
// Per-turn cost (0026): a turn's record goes to trace.jsonl, and a turn that
|
|
2446
|
+
// reaches the threshold also gets one line in plugin.log. Attached before the
|
|
2447
|
+
// event listener so no turn can be missed.
|
|
2448
|
+
turnCostTracker = createTurnCostTracker({
|
|
2449
|
+
onReport: (record) => trace(record),
|
|
2450
|
+
onAlert: (line) => log(line),
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2247
2453
|
try {
|
|
2248
2454
|
ctx.effect(
|
|
2249
2455
|
() => {
|
|
@@ -2330,6 +2536,10 @@ export function apply(ctx) {
|
|
|
2330
2536
|
|
|
2331
2537
|
const sessionId = session?.id ?? 'default';
|
|
2332
2538
|
|
|
2539
|
+
// Before any early return below: turn/end is where the cost record is
|
|
2540
|
+
// written, and that branch returns.
|
|
2541
|
+
turnCostTracker?.observe(sessionId, event);
|
|
2542
|
+
|
|
2333
2543
|
// Keep the last reply: a correction only makes sense next to what it targets.
|
|
2334
2544
|
if (typeof event.data?.turn === 'number') currentTurn = event.data.turn;
|
|
2335
2545
|
if (typeof event.data?.step === 'number') currentStep = event.data.step;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-soulbridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.113",
|
|
4
4
|
"description": "Inject soulbridge context into DSH sessions and feed user turns back as observations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"setup": "node scripts/install-hooks.mjs"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"soulbridge": ">=0.0.
|
|
45
|
+
"soulbridge": ">=0.0.112 <0.1.0"
|
|
46
46
|
}
|
|
47
47
|
}
|