@bumpyclock/pi-tasque 0.1.0 → 0.2.1

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.
Files changed (34) hide show
  1. package/README.md +168 -140
  2. package/package.json +24 -6
  3. package/src/bridge/bridge-tool.ts +10 -13
  4. package/src/bridge/import-tsq.ts +5 -30
  5. package/src/bridge/promote-todo.ts +1 -4
  6. package/src/bridge/types.ts +11 -12
  7. package/src/durable-tasks/bulk-contract.ts +176 -0
  8. package/src/durable-tasks/cache.ts +1 -4
  9. package/src/durable-tasks/change-command-builder.ts +315 -0
  10. package/src/durable-tasks/handoff-guard.ts +329 -0
  11. package/src/durable-tasks/project.ts +71 -0
  12. package/src/durable-tasks/runner.ts +1 -4
  13. package/src/durable-tasks/status.ts +20 -5
  14. package/src/durable-tasks/task-mappers.ts +160 -0
  15. package/src/durable-tasks/task-schema.ts +193 -0
  16. package/src/durable-tasks/task-tool.ts +197 -0
  17. package/src/durable-tasks/task-validation.ts +123 -0
  18. package/src/durable-tasks/tools-bulk.ts +141 -0
  19. package/src/durable-tasks/tools-change.ts +111 -382
  20. package/src/durable-tasks/tools-claim.ts +15 -43
  21. package/src/durable-tasks/tools-handoff.ts +95 -0
  22. package/src/durable-tasks/tools-query.ts +58 -29
  23. package/src/durable-tasks/tools-spec.ts +230 -0
  24. package/src/durable-tasks/tools-tree-create.ts +221 -0
  25. package/src/guidelines/internal-tools.ts +33 -0
  26. package/src/guidelines/task.ts +5 -0
  27. package/src/guidelines/todo.ts +5 -0
  28. package/src/index.ts +2 -13
  29. package/src/session-todos/state/replay.ts +10 -13
  30. package/src/session-todos/todo-overlay.ts +1 -1
  31. package/src/session-todos/todo.ts +11 -15
  32. package/src/session-todos/tool/types.ts +7 -7
  33. package/src/shared/error-utils.ts +29 -0
  34. package/src/shared/validation.ts +25 -0
@@ -0,0 +1,95 @@
1
+ import type {
2
+ AgentToolResult,
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { errorToolDetails, textToolResult } from "../shared/tool-result.js";
7
+ import {
8
+ collectHandoffStatus,
9
+ type HandoffCheckResult,
10
+ } from "./handoff-guard.js";
11
+
12
+ export async function executeHandoffCheck(
13
+ pi: ExtensionAPI,
14
+ signal: AbortSignal | undefined,
15
+ ctx: ExtensionContext,
16
+ ): Promise<AgentToolResult<unknown>> {
17
+ const result = await collectHandoffStatus({
18
+ pi,
19
+ cwd: ctx.cwd,
20
+ ...(signal != null ? { signal } : {}),
21
+ });
22
+
23
+ if (!result.ok) {
24
+ return textToolResult(
25
+ `Error: ${result.message}`,
26
+ errorToolDetails({ code: result.code, message: result.message }),
27
+ );
28
+ }
29
+
30
+ return textToolResult(formatHandoffText(result), {
31
+ ok: true,
32
+ ready: result.ready,
33
+ ...formatHandoffDetails(result),
34
+ });
35
+ }
36
+
37
+ function formatHandoffText(result: HandoffCheckResult & { ok: true }): string {
38
+ const lines: string[] = [
39
+ result.ready
40
+ ? "Handoff ready: all session todos complete and linked tasks resolved."
41
+ : "Handoff not ready.",
42
+ ];
43
+
44
+ if ("todoBlockers" in result && result.todoBlockers?.length) {
45
+ lines.push("", "Todo blockers:");
46
+ for (const b of result.todoBlockers) {
47
+ lines.push(`- #${b.todoId} "${b.subject}" — ${b.reason}`);
48
+ }
49
+ }
50
+
51
+ if ("linkedBlockers" in result && result.linkedBlockers?.length) {
52
+ lines.push("", "Linked task blockers:");
53
+ for (const b of result.linkedBlockers) {
54
+ lines.push(`- ${b.tsqId} (todo #${b.todoId}) — ${b.status}`);
55
+ }
56
+ }
57
+
58
+ if ("linkedWarnings" in result && result.linkedWarnings?.length) {
59
+ lines.push("", "Warnings:");
60
+ for (const w of result.linkedWarnings) {
61
+ lines.push(`- ${w.tsqId} (todo #${w.todoId}) — ${w.status}`);
62
+ }
63
+ }
64
+
65
+ if ("readErrors" in result && result.readErrors?.length) {
66
+ lines.push("", "Read errors:");
67
+ for (const e of result.readErrors) {
68
+ lines.push(`- ${e.tsqId} — ${e.code}: ${e.message}`);
69
+ }
70
+ }
71
+
72
+ return lines.join("\n");
73
+ }
74
+
75
+ function formatHandoffDetails(
76
+ result: HandoffCheckResult & { ok: true },
77
+ ): Record<string, unknown> {
78
+ const details: Record<string, unknown> = {};
79
+ if (result.projectRoot !== undefined) {
80
+ details.projectRoot = result.projectRoot;
81
+ }
82
+ if ("todoBlockers" in result && result.todoBlockers?.length) {
83
+ details.todoBlockers = result.todoBlockers;
84
+ }
85
+ if ("linkedBlockers" in result && result.linkedBlockers?.length) {
86
+ details.linkedBlockers = result.linkedBlockers;
87
+ }
88
+ if ("linkedWarnings" in result && result.linkedWarnings?.length) {
89
+ details.linkedWarnings = result.linkedWarnings;
90
+ }
91
+ if ("readErrors" in result && result.readErrors?.length) {
92
+ details.readErrors = result.readErrors;
93
+ }
94
+ return details;
95
+ }
@@ -5,6 +5,11 @@ import type {
5
5
  ExtensionContext,
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import { type Static, Type } from "typebox";
8
+ import {
9
+ READ_TASKS_PROMPT_GUIDELINES,
10
+ READ_TASKS_PROMPT_SNIPPET,
11
+ } from "../guidelines/internal-tools.js";
12
+ import { isRecord } from "../shared/error-utils.js";
8
13
  import { truncatedTextToolResult } from "../shared/tool-result.js";
9
14
  import type { TruncatedText } from "../shared/truncation.js";
10
15
  import { runTsqJson } from "./runner.js";
@@ -40,12 +45,12 @@ export type TsqQueryAction = (typeof TSQ_QUERY_ACTIONS)[number];
40
45
  export const TsqQueryParamsSchema = Type.Object(
41
46
  {
42
47
  action: StringEnum(TSQ_QUERY_ACTIONS, {
43
- description: "Read-only Tasque query to run.",
48
+ description: "Read-only durable task query to run.",
44
49
  }),
45
50
  id: Type.Optional(
46
51
  Type.String({
47
52
  description:
48
- "Tasque task id. Required for show, show_with_spec, deps, and notes.",
53
+ "Durable task id. Required for show, spec details, deps, and notes.",
49
54
  }),
50
55
  ),
51
56
  lane: Type.Optional(
@@ -98,15 +103,10 @@ const DEFAULT_QUERY_TIMEOUT_MS = 10_000;
98
103
  export function registerTsqQueryTool(pi: ExtensionAPI): void {
99
104
  pi.registerTool({
100
105
  name: TSQ_QUERY_TOOL_NAME,
101
- label: "Tasque Query",
102
- description:
103
- "Run read-only Tasque durable-task queries through tsq --format json. Does not mutate Tasque state.",
104
- promptSnippet:
105
- "Use tsq_query for fresh read-only Tasque task state such as doctor, ready/open lists, show, deps, notes, tree, and similar lookups.",
106
- promptGuidelines: [
107
- "tsq_query is read-only; use mutation tools for lifecycle or notes.",
108
- "Use show_with_spec only when spec content is needed; regular show is more concise.",
109
- ],
106
+ label: "Task Query",
107
+ description: "Read durable task state. Does not mutate tasks.",
108
+ promptSnippet: READ_TASKS_PROMPT_SNIPPET,
109
+ promptGuidelines: READ_TASKS_PROMPT_GUIDELINES,
110
110
  parameters: TsqQueryParamsSchema,
111
111
  executionMode: "parallel",
112
112
  async execute(
@@ -132,10 +132,11 @@ export async function executeTsqQuery(
132
132
  AgentToolResult<TsqQueryDetails & { readonly truncation: TruncatedText }>
133
133
  > {
134
134
  const argv = buildTsqQueryArgv(params);
135
- const data = await runTsqJson<TsqQueryData>(pi, { cwd: ctx.cwd }, argv, {
135
+ const rawData = await runTsqJson<TsqQueryData>(pi, { cwd: ctx.cwd }, argv, {
136
136
  timeout: DEFAULT_QUERY_TIMEOUT_MS,
137
137
  ...(signal === undefined ? {} : { signal }),
138
138
  });
139
+ const data = normalizeQueryData(params, rawData);
139
140
  const text = formatTsqQueryResult(params, data);
140
141
 
141
142
  const details: TsqQueryDetails = {
@@ -197,6 +198,24 @@ export function buildTsqQueryArgv(params: TsqQueryParams): string[] {
197
198
  }
198
199
  }
199
200
 
201
+ function normalizeQueryData(
202
+ params: TsqQueryParams,
203
+ data: TsqQueryData,
204
+ ): TsqQueryData {
205
+ if (params.action !== "find_tree") {
206
+ return data;
207
+ }
208
+
209
+ const id = params.id?.trim();
210
+ if (id === undefined || id.length === 0) {
211
+ return data;
212
+ }
213
+
214
+ const found = findTaskTreeNode(getTreeRoots(data), id);
215
+ const filtered = found === undefined ? [] : [found];
216
+ return Array.isArray(data) ? filtered : { tree: filtered };
217
+ }
218
+
200
219
  function formatTsqQueryResult(
201
220
  params: TsqQueryParams,
202
221
  data: TsqQueryData,
@@ -229,7 +248,7 @@ function formatDoctor(data: TsqQueryData): string {
229
248
  ? doctor.issues.length
230
249
  : undefined;
231
250
  const parts = [
232
- "Tasque doctor",
251
+ "Task doctor",
233
252
  formatMetric(doctor.tasks, "tasks"),
234
253
  formatMetric(doctor.events, "events"),
235
254
  issueCount === undefined ? undefined : `${issueCount} issues`,
@@ -242,7 +261,7 @@ function formatTaskList(
242
261
  tasks: readonly TsqTask[],
243
262
  ): string {
244
263
  const lines = [
245
- `Tasque ${action}: ${formatCount(tasks.length, "task")}${formatLimitNotice(tasks.length)}`,
264
+ `Task ${action}: ${formatCount(tasks.length, "task")}${formatLimitNotice(tasks.length)}`,
246
265
  ...tasks.slice(0, MAX_RENDERED_ITEMS).map(formatTaskLine),
247
266
  ];
248
267
  return lines.join("\n");
@@ -268,21 +287,19 @@ function formatShow(data: TsqQueryData): string {
268
287
  ]
269
288
  .filter((part): part is string => part !== undefined)
270
289
  .join(" · ");
271
- return [`Tasque task: ${parts}: ${task.title}`, extra]
272
- .filter(Boolean)
273
- .join("\n");
290
+ return [`Task: ${parts}: ${task.title}`, extra].filter(Boolean).join("\n");
274
291
  }
275
- return "Tasque task: no task data returned";
292
+ return "Task: no task data returned";
276
293
  }
277
294
 
278
295
  function formatDeps(data: TsqQueryData): string {
279
296
  const root = (data as Partial<{ root: TsqDepTreeNode }>).root;
280
297
  if (root === undefined) {
281
- return "Tasque deps: no dependency data returned";
298
+ return "Task deps: no dependency data returned";
282
299
  }
283
300
  const lines = flattenDepTree(root).slice(0, MAX_RENDERED_ITEMS);
284
301
  return [
285
- `Tasque deps: ${root.id}${formatLimitNotice(countDepTree(root))}`,
302
+ `Task deps: ${root.id}${formatLimitNotice(countDepTree(root))}`,
286
303
  ...lines.map(
287
304
  ({ node, indent }) =>
288
305
  `${" ".repeat(indent)}${formatTaskLine(node.task)}`,
@@ -295,7 +312,7 @@ function formatNotes(data: TsqQueryData): string {
295
312
  const notes = notesData.notes ?? [];
296
313
  const taskId = notesData.task_id ?? "task";
297
314
  return [
298
- `Tasque notes for ${taskId}: ${formatCount(notes.length, "note")}${formatLimitNotice(notes.length)}`,
315
+ `Task notes for ${taskId}: ${formatCount(notes.length, "note")}${formatLimitNotice(notes.length)}`,
299
316
  ...notes.slice(0, MAX_RENDERED_ITEMS).map((note) => {
300
317
  const firstLine = note.text.split(/\r?\n/u)[0] ?? "";
301
318
  return `${note.ts} ${note.actor}: ${truncateInline(firstLine, 120)}`;
@@ -307,7 +324,7 @@ function formatTree(data: TsqQueryData, label = "tree"): string {
307
324
  const roots = getTreeRoots(data);
308
325
  const flattened = roots.flatMap((root) => flattenTaskTree(root));
309
326
  return [
310
- `Tasque ${label}: ${formatCount(flattened.length, "task")}${formatLimitNotice(flattened.length)}`,
327
+ `Task ${label}: ${formatCount(flattened.length, "task")}${formatLimitNotice(flattened.length)}`,
311
328
  ...flattened
312
329
  .slice(0, MAX_RENDERED_ITEMS)
313
330
  .map(
@@ -321,7 +338,7 @@ function formatSimilar(data: TsqQueryData): string {
321
338
  const similar = data as Partial<TsqSimilarData>;
322
339
  const candidates = similar.candidates ?? [];
323
340
  return [
324
- `Tasque similar: ${formatCount(candidates.length, "candidate")}${formatLimitNotice(candidates.length)}`,
341
+ `Task similar: ${formatCount(candidates.length, "candidate")}${formatLimitNotice(candidates.length)}`,
325
342
  ...candidates.slice(0, MAX_RENDERED_ITEMS).map(formatCandidateLine),
326
343
  ].join("\n");
327
344
  }
@@ -342,6 +359,22 @@ function getTreeRoots(data: TsqQueryData): readonly TsqTaskTreeNode[] {
342
359
  return Array.isArray(tree) ? tree.filter(isTaskTreeNode) : [];
343
360
  }
344
361
 
362
+ function findTaskTreeNode(
363
+ roots: readonly TsqTaskTreeNode[],
364
+ id: string,
365
+ ): TsqTaskTreeNode | undefined {
366
+ for (const root of roots) {
367
+ if (root.task.id === id) {
368
+ return root;
369
+ }
370
+ const child = findTaskTreeNode(root.children, id);
371
+ if (child !== undefined) {
372
+ return child;
373
+ }
374
+ }
375
+ return undefined;
376
+ }
377
+
345
378
  function hasTreeData(data: TsqQueryData): boolean {
346
379
  return Array.isArray(data)
347
380
  ? data.some(isTaskTreeNode)
@@ -423,7 +456,7 @@ function validateDepth(depth: unknown): number | undefined {
423
456
  return undefined;
424
457
  }
425
458
  if (typeof depth !== "number" || !Number.isInteger(depth) || depth < 1) {
426
- throw new Error("tsq_query depth must be an integer >= 1");
459
+ throw new Error("task dependency depth must be an integer >= 1");
427
460
  }
428
461
  return depth;
429
462
  }
@@ -435,7 +468,7 @@ function requireString(
435
468
  ): string {
436
469
  const trimmed = value?.trim();
437
470
  if (trimmed === undefined || trimmed.length === 0) {
438
- throw new Error(`tsq_query action ${action} requires ${field}`);
471
+ throw new Error(`task action ${action} requires ${field}`);
439
472
  }
440
473
  return trimmed;
441
474
  }
@@ -490,7 +523,3 @@ function isTaskTreeNode(value: unknown): value is TsqTaskTreeNode {
490
523
  isRecord(value) && isTsqTask(value.task) && Array.isArray(value.children)
491
524
  );
492
525
  }
493
-
494
- function isRecord(value: unknown): value is Record<string, unknown> {
495
- return typeof value === "object" && value !== null && !Array.isArray(value);
496
- }
@@ -0,0 +1,230 @@
1
+ import type {
2
+ AgentToolResult,
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { errorToolDetails, textToolResult } from "../shared/tool-result.js";
7
+ import { runQueuedMutation } from "./mutation-queue.js";
8
+ import { runTsqJson } from "./runner.js";
9
+
10
+ export type SpecMode = "show" | "check" | "set" | "update";
11
+
12
+ export const SPEC_READ_MODES: readonly SpecMode[] = ["show", "check"];
13
+ export const SPEC_WRITE_MODES: readonly SpecMode[] = ["set", "update"];
14
+
15
+ export interface SpecParams {
16
+ readonly id: string | undefined;
17
+ readonly mode: SpecMode;
18
+ readonly text?: string | undefined;
19
+ }
20
+
21
+ export interface SpecSuccessDetails {
22
+ readonly ok: true;
23
+ readonly action: "spec";
24
+ readonly mode: SpecMode;
25
+ readonly argv: readonly string[];
26
+ readonly data: unknown;
27
+ }
28
+
29
+ export interface SpecCheckFailedDetails {
30
+ readonly ok: false;
31
+ readonly error: {
32
+ readonly code: "spec_check_failed";
33
+ readonly message: string;
34
+ readonly details: unknown;
35
+ };
36
+ }
37
+
38
+ export type SpecDetails = SpecSuccessDetails | SpecCheckFailedDetails;
39
+
40
+ const DEFAULT_SPEC_TIMEOUT_MS = 10_000;
41
+
42
+ export async function executeTsqSpec(
43
+ pi: ExtensionAPI,
44
+ params: SpecParams,
45
+ signal: AbortSignal | undefined,
46
+ ctx: Pick<ExtensionContext, "cwd">,
47
+ ): Promise<AgentToolResult<SpecDetails>> {
48
+ const validated = validateSpecParams(params);
49
+ if (!validated.ok) {
50
+ return textToolResult(
51
+ `Error: ${validated.message}`,
52
+ errorToolDetails({
53
+ code: "validation_error",
54
+ message: validated.message,
55
+ }) as unknown as SpecDetails,
56
+ );
57
+ }
58
+
59
+ const { argv, mode } = validated;
60
+ const isWrite = (SPEC_WRITE_MODES as readonly string[]).includes(mode);
61
+
62
+ try {
63
+ const data = isWrite
64
+ ? await runQueuedMutation(ctx.cwd, () =>
65
+ runTsqJson(pi, { cwd: ctx.cwd }, argv, {
66
+ timeout: DEFAULT_SPEC_TIMEOUT_MS,
67
+ ...(signal === undefined ? {} : { signal }),
68
+ }),
69
+ )
70
+ : await runTsqJson(pi, { cwd: ctx.cwd }, argv, {
71
+ timeout: DEFAULT_SPEC_TIMEOUT_MS,
72
+ ...(signal === undefined ? {} : { signal }),
73
+ });
74
+
75
+ if (mode === "check" && isSpecCheckFailed(data)) {
76
+ return buildCheckFailedResult(data, argv);
77
+ }
78
+
79
+ return textToolResult(formatSpecSuccess(mode, data), {
80
+ ok: true,
81
+ action: "spec",
82
+ mode,
83
+ argv,
84
+ data,
85
+ } as SpecSuccessDetails);
86
+ } catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ const code = getErrorCode(error);
89
+ return textToolResult(
90
+ `Error: ${message}`,
91
+ errorToolDetails({
92
+ code,
93
+ message,
94
+ details: { action: "spec", mode, argv },
95
+ }) as unknown as SpecDetails,
96
+ );
97
+ }
98
+ }
99
+
100
+ type ValidationSuccess = {
101
+ readonly ok: true;
102
+ readonly mode: SpecMode;
103
+ readonly argv: string[];
104
+ };
105
+ type ValidationFailure = { readonly ok: false; readonly message: string };
106
+ type ValidationResult = ValidationSuccess | ValidationFailure;
107
+
108
+ export function validateSpecParams(params: SpecParams): ValidationResult {
109
+ const id = params.id?.trim();
110
+ if (id === undefined || id.length === 0) {
111
+ return { ok: false, message: "spec action requires id" };
112
+ }
113
+
114
+ const { mode } = params;
115
+ const isRead = (SPEC_READ_MODES as readonly string[]).includes(mode);
116
+ const isWrite = (SPEC_WRITE_MODES as readonly string[]).includes(mode);
117
+
118
+ if (!isRead && !isWrite) {
119
+ return {
120
+ ok: false,
121
+ message: `spec mode must be show, check, set, or update`,
122
+ };
123
+ }
124
+
125
+ if (isRead && params.text !== undefined) {
126
+ return {
127
+ ok: false,
128
+ message: `spec ${mode} does not accept text`,
129
+ };
130
+ }
131
+
132
+ if (isWrite) {
133
+ const text = params.text?.trim();
134
+ if (text === undefined || text.length === 0) {
135
+ return { ok: false, message: `spec ${mode} requires text` };
136
+ }
137
+ }
138
+
139
+ return { ok: true, mode, argv: buildSpecArgv(id, mode, params.text) };
140
+ }
141
+
142
+ export function buildSpecArgv(
143
+ id: string,
144
+ mode: SpecMode,
145
+ text: string | undefined,
146
+ ): string[] {
147
+ switch (mode) {
148
+ case "show":
149
+ return ["spec", id, "--show"];
150
+ case "check":
151
+ return ["spec", id, "--check"];
152
+ case "set":
153
+ return ["spec", id, "--force", `--text=${text!}`];
154
+ case "update":
155
+ return ["spec", id, "--update", `--text=${text!}`];
156
+ }
157
+ }
158
+
159
+ function isSpecCheckFailed(data: unknown): boolean {
160
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
161
+ return false;
162
+ }
163
+ return (data as Record<string, unknown>).ok === false;
164
+ }
165
+
166
+ function buildCheckFailedResult(
167
+ data: unknown,
168
+ argv: readonly string[],
169
+ ): AgentToolResult<SpecDetails> {
170
+ const record = data as Record<string, unknown>;
171
+ const diagnostics = record.diagnostics ?? record.issues ?? record;
172
+ const message =
173
+ typeof record.message === "string" ? record.message : "spec check failed";
174
+
175
+ return textToolResult(`Spec check failed: ${message}`, {
176
+ ok: false,
177
+ error: {
178
+ code: "spec_check_failed",
179
+ message,
180
+ details: { argv, diagnostics },
181
+ },
182
+ } as SpecCheckFailedDetails);
183
+ }
184
+
185
+ function formatSpecSuccess(mode: SpecMode, data: unknown): string {
186
+ switch (mode) {
187
+ case "show": {
188
+ const record = data as Record<string, unknown> | null;
189
+ const spec =
190
+ typeof record?.spec === "object" &&
191
+ record.spec !== null &&
192
+ !Array.isArray(record.spec)
193
+ ? (record.spec as Record<string, unknown>)
194
+ : undefined;
195
+ const content =
196
+ typeof spec?.content === "string"
197
+ ? spec.content
198
+ : typeof record?.content === "string"
199
+ ? record.content
200
+ : undefined;
201
+ const path =
202
+ typeof spec?.path === "string"
203
+ ? spec.path
204
+ : typeof record?.path === "string"
205
+ ? record.path
206
+ : undefined;
207
+ if (content !== undefined) {
208
+ const header = path !== undefined ? `Spec (${path}):\n` : "Spec:\n";
209
+ return `${header}${content}`;
210
+ }
211
+ return "Spec: no content returned";
212
+ }
213
+ case "check":
214
+ return "Spec check passed";
215
+ case "set":
216
+ return "Spec attached";
217
+ case "update":
218
+ return "Spec updated";
219
+ }
220
+ }
221
+
222
+ function getErrorCode(error: unknown): string {
223
+ if (typeof error === "object" && error !== null && !Array.isArray(error)) {
224
+ const code = (error as Record<string, unknown>).code;
225
+ if (typeof code === "string") {
226
+ return code;
227
+ }
228
+ }
229
+ return "tsq_error";
230
+ }