@tekyzinc/gsd-t 3.10.13 → 3.10.14
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
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [3.10.14] - 2026-04-15
|
|
6
|
+
|
|
7
|
+
### Fixed — transcript parser orphaned tool_use blocks cause count_tokens 400
|
|
8
|
+
|
|
9
|
+
**Background**: With `ANTHROPIC_API_KEY` now set (v3.10.12-13 fix), the context meter hook passed the key check but the `count_tokens` API returned HTTP 400: `"tool_use ids were found without tool_result blocks immediately after"`. The transcript parser (`scripts/context-meter/transcript-parser.js`) faithfully reconstructed the JSONL transcript but didn't enforce the API's strict adjacency constraint: every assistant `tool_use` must be immediately followed by a user `tool_result` with matching ids. Mid-session compaction and summarization can orphan these blocks.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **`scripts/context-meter/transcript-parser.js`** — added `sanitizeToolPairs()` post-processing pass after message reconstruction. Walks the message list enforcing adjacency: assistant `tool_use` blocks are kept only if the immediately following user message contains a `tool_result` with a matching id, and vice versa. Messages that become empty after stripping are dropped. This is a structural fix — any transcript shape (compacted, summarized, interrupted) now produces a valid `count_tokens` payload.
|
|
13
|
+
- **`scripts/context-meter/transcript-parser.test.js`** — updated 2 existing tests that created orphaned `tool_result` messages (now include matching `tool_use` predecessors). Added 1 new test: `orphaned tool_use without matching tool_result is stripped`. 1229/1229 tests pass.
|
|
14
|
+
|
|
15
|
+
### Verification
|
|
16
|
+
- Real transcript (626→649 messages, 473KB payload) now returns HTTP 200 with `input_tokens: 153597` (was 400 before fix)
|
|
17
|
+
- Context meter state file flipped from `lastError: api_error` to `lastError: null`, `inputTokens: 158543`, `pct: 79.3%`, `threshold: warn`
|
|
18
|
+
- First successful real-time context measurement since M34 was built
|
|
19
|
+
|
|
5
20
|
## [3.10.13] - 2026-04-15
|
|
6
21
|
|
|
7
22
|
### Fixed — P0 v3.10.12 propagation gap (same regression, downstream projects)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.14",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 61 slash commands with unattended supervisor relay, headless CI/CD mode, graph-powered code analysis, real-time agent dashboard, execution intelligence, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -152,7 +152,69 @@ async function parseTranscript(transcriptPath) {
|
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
return { system, messages };
|
|
155
|
+
return { system, messages: sanitizeToolPairs(messages) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Enforce the count_tokens adjacency constraint: every assistant tool_use
|
|
160
|
+
* must be immediately followed by a user message whose tool_result ids match
|
|
161
|
+
* ALL tool_use ids from the preceding assistant message. Walk the message
|
|
162
|
+
* list and strip tool_use / tool_result blocks from any pair that violates
|
|
163
|
+
* this rule. Drop messages that become empty after stripping.
|
|
164
|
+
*/
|
|
165
|
+
function sanitizeToolPairs(messages) {
|
|
166
|
+
const out = [];
|
|
167
|
+
for (let i = 0; i < messages.length; i++) {
|
|
168
|
+
const m = messages[i];
|
|
169
|
+
if (!Array.isArray(m.content)) { out.push(m); continue; }
|
|
170
|
+
|
|
171
|
+
if (m.role === "assistant") {
|
|
172
|
+
const toolUseIds = new Set();
|
|
173
|
+
for (const b of m.content) {
|
|
174
|
+
if (b.type === "tool_use" && typeof b.id === "string") toolUseIds.add(b.id);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (toolUseIds.size === 0) { out.push(m); continue; }
|
|
178
|
+
|
|
179
|
+
const next = messages[i + 1];
|
|
180
|
+
const nextResultIds = new Set();
|
|
181
|
+
if (next && next.role === "user" && Array.isArray(next.content)) {
|
|
182
|
+
for (const b of next.content) {
|
|
183
|
+
if (b.type === "tool_result" && typeof b.tool_use_id === "string") {
|
|
184
|
+
nextResultIds.add(b.tool_use_id);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const validIds = new Set([...toolUseIds].filter((id) => nextResultIds.has(id)));
|
|
190
|
+
const filtered = m.content.filter((b) => {
|
|
191
|
+
if (b.type === "tool_use") return validIds.has(b.id);
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
if (filtered.length > 0) out.push({ role: m.role, content: filtered });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (m.role === "user") {
|
|
199
|
+
const prev = out[out.length - 1];
|
|
200
|
+
const prevUseIds = new Set();
|
|
201
|
+
if (prev && prev.role === "assistant" && Array.isArray(prev.content)) {
|
|
202
|
+
for (const b of prev.content) {
|
|
203
|
+
if (b.type === "tool_use" && typeof b.id === "string") prevUseIds.add(b.id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const filtered = m.content.filter((b) => {
|
|
208
|
+
if (b.type === "tool_result") return prevUseIds.has(b.tool_use_id);
|
|
209
|
+
return true;
|
|
210
|
+
});
|
|
211
|
+
if (filtered.length > 0) out.push({ role: m.role, content: filtered });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
out.push(m);
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
156
218
|
}
|
|
157
219
|
|
|
158
220
|
/**
|
|
@@ -176,6 +176,15 @@ test("tool_use / tool_result pairing by tool_use_id preserved in order", async (
|
|
|
176
176
|
|
|
177
177
|
test("tool_result with array content (text blocks) is normalized", async () => {
|
|
178
178
|
const { dir, file } = mkTmpFile([
|
|
179
|
+
{
|
|
180
|
+
type: "assistant",
|
|
181
|
+
message: {
|
|
182
|
+
role: "assistant",
|
|
183
|
+
content: [
|
|
184
|
+
{ type: "tool_use", id: "toolu_02", name: "Read", input: { file_path: "/tmp/x" } },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
179
188
|
{
|
|
180
189
|
type: "user",
|
|
181
190
|
message: {
|
|
@@ -196,8 +205,8 @@ test("tool_result with array content (text blocks) is normalized", async () => {
|
|
|
196
205
|
]);
|
|
197
206
|
try {
|
|
198
207
|
const got = await parseTranscript(file);
|
|
199
|
-
|
|
200
|
-
const tr =
|
|
208
|
+
const userMsg = got.messages.find(m => m.role === "user");
|
|
209
|
+
const tr = userMsg.content[0];
|
|
201
210
|
assert.equal(tr.type, "tool_result");
|
|
202
211
|
assert.deepEqual(tr.content, [
|
|
203
212
|
{ type: "text", text: "line 1" },
|
|
@@ -210,6 +219,15 @@ test("tool_result with array content (text blocks) is normalized", async () => {
|
|
|
210
219
|
|
|
211
220
|
test("tool_result with is_error:true preserved", async () => {
|
|
212
221
|
const { dir, file } = mkTmpFile([
|
|
222
|
+
{
|
|
223
|
+
type: "assistant",
|
|
224
|
+
message: {
|
|
225
|
+
role: "assistant",
|
|
226
|
+
content: [
|
|
227
|
+
{ type: "tool_use", id: "toolu_03", name: "Bash", input: { command: "false" } },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
213
231
|
{
|
|
214
232
|
type: "user",
|
|
215
233
|
message: {
|
|
@@ -222,7 +240,8 @@ test("tool_result with is_error:true preserved", async () => {
|
|
|
222
240
|
]);
|
|
223
241
|
try {
|
|
224
242
|
const got = await parseTranscript(file);
|
|
225
|
-
|
|
243
|
+
const userMsg = got.messages.find(m => m.role === "user");
|
|
244
|
+
assert.equal(userMsg.content[0].is_error, true);
|
|
226
245
|
} finally {
|
|
227
246
|
cleanup(dir);
|
|
228
247
|
}
|
|
@@ -318,3 +337,34 @@ test("message with content array but no recognized blocks → message skipped",
|
|
|
318
337
|
cleanup(dir);
|
|
319
338
|
}
|
|
320
339
|
});
|
|
340
|
+
|
|
341
|
+
test("orphaned tool_use without matching tool_result is stripped", async () => {
|
|
342
|
+
const { dir, file } = mkTmpFile([
|
|
343
|
+
{ type: "user", message: { role: "user", content: "hello" } },
|
|
344
|
+
{
|
|
345
|
+
type: "assistant",
|
|
346
|
+
message: {
|
|
347
|
+
role: "assistant",
|
|
348
|
+
content: [
|
|
349
|
+
{ type: "text", text: "I will read a file" },
|
|
350
|
+
{ type: "tool_use", id: "toolu_orphan", name: "Read", input: { file_path: "/tmp/x" } },
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{ type: "user", message: { role: "user", content: "thanks" } },
|
|
355
|
+
]);
|
|
356
|
+
try {
|
|
357
|
+
const got = await parseTranscript(file);
|
|
358
|
+
const assistantMsg = got.messages.find(
|
|
359
|
+
(m) => m.role === "assistant" && m.content.some((b) => b.type === "text")
|
|
360
|
+
);
|
|
361
|
+
assert.ok(assistantMsg, "assistant message preserved");
|
|
362
|
+
assert.ok(
|
|
363
|
+
!assistantMsg.content.some((b) => b.type === "tool_use"),
|
|
364
|
+
"orphaned tool_use stripped"
|
|
365
|
+
);
|
|
366
|
+
assert.equal(assistantMsg.content[0].text, "I will read a file");
|
|
367
|
+
} finally {
|
|
368
|
+
cleanup(dir);
|
|
369
|
+
}
|
|
370
|
+
});
|