kongbrain 0.3.16 → 0.4.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.
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/acan.ts +4 -1
- package/src/cognitive-check.ts +2 -2
- package/src/concept-extract.ts +1 -1
- package/src/context-engine.ts +128 -4
- package/src/daemon-manager.ts +17 -11
- package/src/deferred-cleanup.ts +3 -7
- package/src/graph-context.ts +220 -69
- package/src/handoff-file.ts +12 -5
- package/src/hooks/after-tool-call.ts +3 -3
- package/src/hooks/before-tool-call.ts +48 -1
- package/src/hooks/llm-output.ts +28 -8
- package/src/hooks/subagent-lifecycle.ts +142 -0
- package/src/index.ts +11 -2
- package/src/orchestrator.ts +1 -1
- package/src/reflection.ts +1 -0
- package/src/soul.ts +1 -1
- package/src/state.ts +18 -0
- package/src/surreal.ts +4 -0
- package/src/tools/core-memory.ts +9 -1
- package/src/tools/recall.ts +3 -3
package/src/hooks/llm-output.ts
CHANGED
|
@@ -33,14 +33,34 @@ export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
|
33
33
|
// Measure assistant text output (used for token estimation and planning gate)
|
|
34
34
|
const textLen = event.assistantTexts.reduce((s, t) => s + t.length, 0);
|
|
35
35
|
|
|
36
|
-
// Extract token counts —
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
// Extract token counts — OpenClaw's getUsageTotals() returns CUMULATIVE totals
|
|
37
|
+
// across all API calls in the session, not per-response values.
|
|
38
|
+
// Compute the delta since last call to avoid quadratic overcounting.
|
|
39
|
+
const reportedInput = event.usage?.input ?? 0;
|
|
40
|
+
const reportedOutput = event.usage?.output ?? 0;
|
|
41
|
+
const reportedCacheRead = event.usage?.cacheRead ?? 0;
|
|
42
|
+
const reportedCacheWrite = event.usage?.cacheWrite ?? 0;
|
|
43
|
+
const reportedTotal = reportedInput + reportedOutput + reportedCacheRead + reportedCacheWrite;
|
|
44
|
+
|
|
45
|
+
let deltaTokens: number;
|
|
46
|
+
if (reportedTotal > 0) {
|
|
47
|
+
deltaTokens = Math.max(0, reportedTotal - session.lastSeenUsageTotal);
|
|
48
|
+
session.lastSeenUsageTotal = reportedTotal;
|
|
49
|
+
} else if (textLen > 0) {
|
|
50
|
+
// No usage data — fall back to text-length estimate
|
|
51
|
+
deltaTokens = Math.ceil(textLen / 4); // ~4 chars per token
|
|
52
|
+
} else {
|
|
53
|
+
deltaTokens = 0;
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
// DB stats: approximate input/output split from the delta
|
|
57
|
+
const inputTokens = reportedTotal > 0 && deltaTokens > 0
|
|
58
|
+
? Math.round(deltaTokens * (reportedInput / reportedTotal))
|
|
59
|
+
: 0;
|
|
60
|
+
const outputTokens = reportedTotal > 0 && deltaTokens > 0
|
|
61
|
+
? Math.round(deltaTokens * (reportedOutput / reportedTotal))
|
|
62
|
+
: (deltaTokens > 0 ? deltaTokens : Math.ceil(textLen / 4));
|
|
63
|
+
|
|
44
64
|
// Always update session stats — turn_count must increment even without usage data
|
|
45
65
|
if (session.surrealSessionId) {
|
|
46
66
|
try {
|
|
@@ -55,8 +75,8 @@ export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
// Accumulate for daemon batching and mid-session cleanup
|
|
58
|
-
session.newContentTokens +=
|
|
59
|
-
session.cumulativeTokens +=
|
|
78
|
+
session.newContentTokens += deltaTokens;
|
|
79
|
+
session.cumulativeTokens += deltaTokens;
|
|
60
80
|
|
|
61
81
|
// Track accumulated text output for planning gate
|
|
62
82
|
session.turnTextLength += textLen;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent_spawned / subagent_ended hooks — track spawned subagents in the graph.
|
|
3
|
+
*
|
|
4
|
+
* Creates `subagent` records and `spawned` edges (session → subagent).
|
|
5
|
+
* Updates subagent records with outcome on completion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { GlobalPluginState } from "../state.js";
|
|
9
|
+
import { swallow } from "../errors.js";
|
|
10
|
+
|
|
11
|
+
// ── Event shapes (from OpenClaw gateway) ─────────────────────────────────
|
|
12
|
+
|
|
13
|
+
interface SubagentSpawnedEvent {
|
|
14
|
+
runId: string;
|
|
15
|
+
childSessionKey: string;
|
|
16
|
+
agentId?: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
requester?: {
|
|
19
|
+
channel?: string;
|
|
20
|
+
accountId?: string;
|
|
21
|
+
to?: string;
|
|
22
|
+
threadId?: string;
|
|
23
|
+
};
|
|
24
|
+
threadRequested?: boolean;
|
|
25
|
+
mode?: string; // "run" | "session"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SubagentSpawnedContext {
|
|
29
|
+
runId: string;
|
|
30
|
+
childSessionKey: string;
|
|
31
|
+
requesterSessionKey?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SubagentEndedEvent {
|
|
35
|
+
targetSessionKey: string;
|
|
36
|
+
targetKind?: string;
|
|
37
|
+
reason?: string;
|
|
38
|
+
sendFarewell?: boolean;
|
|
39
|
+
accountId?: string;
|
|
40
|
+
runId: string;
|
|
41
|
+
endedAt?: string;
|
|
42
|
+
outcome?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SubagentEndedContext {
|
|
47
|
+
runId: string;
|
|
48
|
+
childSessionKey: string;
|
|
49
|
+
requesterSessionKey?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Handlers ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export function createSubagentSpawnedHandler(state: GlobalPluginState) {
|
|
55
|
+
return async (event: SubagentSpawnedEvent, ctx: SubagentSpawnedContext) => {
|
|
56
|
+
try {
|
|
57
|
+
const store = state.store;
|
|
58
|
+
|
|
59
|
+
// Create the subagent record
|
|
60
|
+
const rows = await store.queryFirst<{ id: string }>(
|
|
61
|
+
`CREATE subagent CONTENT {
|
|
62
|
+
run_id: $run_id,
|
|
63
|
+
parent_session_key: $parent_key,
|
|
64
|
+
child_session_key: $child_key,
|
|
65
|
+
parent_session_id: $parent_key,
|
|
66
|
+
child_session_id: $child_key,
|
|
67
|
+
agent_id: $agent_id,
|
|
68
|
+
label: $label,
|
|
69
|
+
mode: $mode,
|
|
70
|
+
task: $label,
|
|
71
|
+
status: "running",
|
|
72
|
+
created_at: time::now()
|
|
73
|
+
} RETURN id`,
|
|
74
|
+
{
|
|
75
|
+
run_id: event.runId,
|
|
76
|
+
parent_key: ctx.requesterSessionKey ?? "unknown",
|
|
77
|
+
child_key: event.childSessionKey,
|
|
78
|
+
agent_id: event.agentId ?? "default",
|
|
79
|
+
label: event.label ?? null,
|
|
80
|
+
mode: event.mode ?? "run",
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const subagentId = String(rows[0]?.id ?? "");
|
|
85
|
+
if (!subagentId) return;
|
|
86
|
+
|
|
87
|
+
// Find the parent's surreal session ID to create the spawned edge.
|
|
88
|
+
// The requesterSessionKey is the OpenClaw session key — we need to
|
|
89
|
+
// find the matching surreal session record.
|
|
90
|
+
if (ctx.requesterSessionKey) {
|
|
91
|
+
// Look up active session state first (fast path)
|
|
92
|
+
const parentSession = state.getSession(ctx.requesterSessionKey);
|
|
93
|
+
if (parentSession?.surrealSessionId) {
|
|
94
|
+
await store.relate(parentSession.surrealSessionId, "spawned", subagentId);
|
|
95
|
+
} else {
|
|
96
|
+
// Fallback: find the most recent session record that's still active
|
|
97
|
+
const sessions = await store.queryFirst<{ id: string }>(
|
|
98
|
+
`SELECT id FROM session
|
|
99
|
+
WHERE ended_at IS NONE
|
|
100
|
+
ORDER BY started_at DESC LIMIT 1`,
|
|
101
|
+
);
|
|
102
|
+
if (sessions.length > 0) {
|
|
103
|
+
await store.relate(String(sessions[0].id), "spawned", subagentId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
swallow.warn("hook:subagentSpawned", e);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createSubagentEndedHandler(state: GlobalPluginState) {
|
|
114
|
+
return async (event: SubagentEndedEvent, ctx: SubagentEndedContext) => {
|
|
115
|
+
try {
|
|
116
|
+
const store = state.store;
|
|
117
|
+
|
|
118
|
+
// Update the subagent record by run_id
|
|
119
|
+
await store.queryExec(
|
|
120
|
+
`UPDATE subagent SET
|
|
121
|
+
status = $status,
|
|
122
|
+
outcome = $outcome,
|
|
123
|
+
error = $error,
|
|
124
|
+
reason = $reason,
|
|
125
|
+
ended_at = $ended_at
|
|
126
|
+
WHERE run_id = $run_id`,
|
|
127
|
+
{
|
|
128
|
+
run_id: event.runId,
|
|
129
|
+
status: event.outcome === "success" ? "completed"
|
|
130
|
+
: event.reason === "spawn-failed" ? "error"
|
|
131
|
+
: event.outcome ?? "completed",
|
|
132
|
+
outcome: event.outcome ?? null,
|
|
133
|
+
error: event.error ?? null,
|
|
134
|
+
reason: event.reason ?? null,
|
|
135
|
+
ended_at: event.endedAt ?? new Date().toISOString(),
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
swallow.warn("hook:subagentEnded", e);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { createBeforePromptBuildHandler } from "./hooks/before-prompt-build.js";
|
|
|
20
20
|
import { createBeforeToolCallHandler } from "./hooks/before-tool-call.js";
|
|
21
21
|
import { createAfterToolCallHandler } from "./hooks/after-tool-call.js";
|
|
22
22
|
import { createLlmOutputHandler } from "./hooks/llm-output.js";
|
|
23
|
+
import { createSubagentSpawnedHandler, createSubagentEndedHandler } from "./hooks/subagent-lifecycle.js";
|
|
23
24
|
import { startMemoryDaemon } from "./daemon-manager.js";
|
|
24
25
|
import { seedIdentity } from "./identity.js";
|
|
25
26
|
import { seedCognitiveBootstrap } from "./cognitive-bootstrap.js";
|
|
@@ -146,11 +147,18 @@ async function runSessionCleanup(
|
|
|
146
147
|
new Promise(resolve => setTimeout(resolve, 150_000)),
|
|
147
148
|
]);
|
|
148
149
|
|
|
150
|
+
// Await the graduation promise once and reuse the result below
|
|
151
|
+
let gradResult: Awaited<typeof graduationPromise> = null;
|
|
152
|
+
try {
|
|
153
|
+
gradResult = await graduationPromise;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
swallow.warn("cleanup:graduationAwait", e);
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
// If soul graduation just happened, persist a graduation event so the next
|
|
150
159
|
// session can celebrate with the user. We also fire a system event for
|
|
151
160
|
// immediate visibility if the session is still active.
|
|
152
161
|
try {
|
|
153
|
-
const gradResult = await graduationPromise;
|
|
154
162
|
if (gradResult?.graduated && gradResult.soul) {
|
|
155
163
|
// Check if this is a NEW graduation (not a pre-existing soul)
|
|
156
164
|
const isNewGraduation = gradResult.report.stage === "ready";
|
|
@@ -188,7 +196,6 @@ async function runSessionCleanup(
|
|
|
188
196
|
// Soul evolution — if soul already exists, check if it should be revised
|
|
189
197
|
// based on new experience (runs every 10 sessions after last revision)
|
|
190
198
|
try {
|
|
191
|
-
const gradResult = await graduationPromise;
|
|
192
199
|
if (gradResult?.graduated && gradResult.report.stage !== "ready") {
|
|
193
200
|
// Pre-existing soul — check for evolution
|
|
194
201
|
await evolveSoul(s, complete);
|
|
@@ -421,6 +428,8 @@ export default definePluginEntry({
|
|
|
421
428
|
api.on("before_tool_call", createBeforeToolCallHandler(globalState));
|
|
422
429
|
api.on("after_tool_call", createAfterToolCallHandler(globalState));
|
|
423
430
|
api.on("llm_output", createLlmOutputHandler(globalState));
|
|
431
|
+
api.on("subagent_spawned", createSubagentSpawnedHandler(globalState));
|
|
432
|
+
api.on("subagent_ended", createSubagentEndedHandler(globalState));
|
|
424
433
|
}
|
|
425
434
|
|
|
426
435
|
// ── Session lifecycle (also register once) ─────────────────────────
|
package/src/orchestrator.ts
CHANGED
|
@@ -192,7 +192,7 @@ export async function preflight(
|
|
|
192
192
|
|
|
193
193
|
// Non-first-turn short inputs → continuation
|
|
194
194
|
if (orch.turnIndex > 1 && input.length < 20 && !input.includes("?")) {
|
|
195
|
-
const inheritedLimit = Math.
|
|
195
|
+
const inheritedLimit = Math.min(orch.lastConfig.toolLimit, 25);
|
|
196
196
|
const config: AdaptiveConfig = {
|
|
197
197
|
...orch.lastConfig, toolLimit: inheritedLimit, skipRetrieval: true,
|
|
198
198
|
vectorSearchLimits: { turn: 0, identity: 0, concept: 0, memory: 0, artifact: 0 },
|
package/src/reflection.ts
CHANGED
|
@@ -207,6 +207,7 @@ export async function generateReflection(
|
|
|
207
207
|
{ record },
|
|
208
208
|
);
|
|
209
209
|
const reflectionId = String(rows[0]?.id ?? "");
|
|
210
|
+
store.clearReflectionCache();
|
|
210
211
|
|
|
211
212
|
if (reflectionId && surrealSessionId) {
|
|
212
213
|
await store.relate(reflectionId, "reflects_on", surrealSessionId).catch(e => swallow.warn("reflection:relate", e));
|
package/src/soul.ts
CHANGED
|
@@ -191,7 +191,7 @@ export async function getQualitySignals(store: SurrealStore): Promise<QualitySig
|
|
|
191
191
|
const totalSuccess = Number(skillRow?.totalSuccess ?? 0);
|
|
192
192
|
const totalFailure = Number(skillRow?.totalFailure ?? 0);
|
|
193
193
|
const skillTotal = totalSuccess + totalFailure;
|
|
194
|
-
const skillSuccessRate = skillTotal > 0 ? totalSuccess / skillTotal : 0;
|
|
194
|
+
const skillSuccessRate = skillTotal > 0 && Number.isFinite(skillTotal) ? totalSuccess / skillTotal : 0;
|
|
195
195
|
|
|
196
196
|
const critCount = Number(critRow?.count ?? 0);
|
|
197
197
|
const reflCount = Number(totalRow?.count ?? 0);
|
package/src/state.ts
CHANGED
|
@@ -62,6 +62,9 @@ export class SessionState {
|
|
|
62
62
|
cumulativeTokens = 0;
|
|
63
63
|
lastCleanupTokens = 0;
|
|
64
64
|
midSessionCleanupThreshold = 25_000;
|
|
65
|
+
/** Last cumulative usage total seen from OpenClaw — used to compute per-call deltas
|
|
66
|
+
* since getUsageTotals() returns running totals, not per-response values. */
|
|
67
|
+
lastSeenUsageTotal = 0;
|
|
65
68
|
|
|
66
69
|
// Cleanup tracking
|
|
67
70
|
cleanedUp = false;
|
|
@@ -72,6 +75,17 @@ export class SessionState {
|
|
|
72
75
|
// Pending tool args for artifact tracking
|
|
73
76
|
readonly pendingToolArgs = new Map<string, unknown>();
|
|
74
77
|
|
|
78
|
+
// Tool call optimization state (claw-code patterns)
|
|
79
|
+
/** Query vector from this turn's context retrieval — used to detect redundant recall calls. */
|
|
80
|
+
lastQueryVec: number[] | null = null;
|
|
81
|
+
/** Summary of what graphTransformContext injected — shown in planning gate. */
|
|
82
|
+
lastRetrievalSummary = "";
|
|
83
|
+
/** API request cycle counter — hard cap prevents runaway token spend. */
|
|
84
|
+
apiCycleCount = 0;
|
|
85
|
+
/** Tracks which static context sections the model has already seen in the conversation window.
|
|
86
|
+
* Persists across turns (NOT cleared in resetTurn) — cleared only when messages drop from window. */
|
|
87
|
+
readonly injectedSections = new Set<string>();
|
|
88
|
+
|
|
75
89
|
// 5-pillar IDs (populated at bootstrap)
|
|
76
90
|
agentId = "";
|
|
77
91
|
projectId = "";
|
|
@@ -92,6 +106,10 @@ export class SessionState {
|
|
|
92
106
|
this.softInterrupted = false;
|
|
93
107
|
this.turnStartMs = Date.now();
|
|
94
108
|
this.pendingThinking.length = 0;
|
|
109
|
+
this.lastRetrievalSummary = "";
|
|
110
|
+
this.apiCycleCount = 0;
|
|
111
|
+
// NOTE: lastQueryVec and injectedSections are NOT cleared here —
|
|
112
|
+
// they persist across turns within the session.
|
|
95
113
|
}
|
|
96
114
|
}
|
|
97
115
|
|
package/src/surreal.ts
CHANGED
|
@@ -1408,6 +1408,10 @@ export class SurrealStore {
|
|
|
1408
1408
|
|
|
1409
1409
|
private _reflectionSessions: Set<string> | null = null;
|
|
1410
1410
|
|
|
1411
|
+
clearReflectionCache(): void {
|
|
1412
|
+
this._reflectionSessions = null;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1411
1415
|
async getReflectionSessionIds(): Promise<Set<string>> {
|
|
1412
1416
|
if (this._reflectionSessions) return this._reflectionSessions;
|
|
1413
1417
|
try {
|
package/src/tools/core-memory.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function createCoreMemoryToolDef(state: GlobalPluginState, session: Sessi
|
|
|
46
46
|
}
|
|
47
47
|
const formatted = entries.map((e, i) => {
|
|
48
48
|
const sid = e.session_id ? ` session:${e.session_id}` : "";
|
|
49
|
-
return `${i + 1}. [T${e.tier}/${e.category}/p${e.priority}${sid}] ${e.id}\n ${e.text.slice(0,
|
|
49
|
+
return `${i + 1}. [T${e.tier}/${e.category}/p${e.priority}${sid}] ${e.id}\n ${e.text.slice(0, 120)}`;
|
|
50
50
|
}).join("\n\n");
|
|
51
51
|
return {
|
|
52
52
|
content: [{ type: "text" as const, text: `${entries.length} core memory entries:\n\n${formatted}` }],
|
|
@@ -73,6 +73,8 @@ export function createCoreMemoryToolDef(state: GlobalPluginState, session: Sessi
|
|
|
73
73
|
details: { error: true },
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
// Invalidate cached section so updated content re-injects next turn
|
|
77
|
+
session.injectedSections.delete(tier === 0 ? "tier0" : "tier1");
|
|
76
78
|
return {
|
|
77
79
|
content: [{ type: "text" as const, text: `Created core memory: ${id} (tier ${tier}, ${params.category ?? "general"}, p${params.priority ?? 50})` }],
|
|
78
80
|
details: { id },
|
|
@@ -95,6 +97,9 @@ export function createCoreMemoryToolDef(state: GlobalPluginState, session: Sessi
|
|
|
95
97
|
details: { error: true },
|
|
96
98
|
};
|
|
97
99
|
}
|
|
100
|
+
// Invalidate both tiers — update may have changed the tier
|
|
101
|
+
session.injectedSections.delete("tier0");
|
|
102
|
+
session.injectedSections.delete("tier1");
|
|
98
103
|
return {
|
|
99
104
|
content: [{ type: "text" as const, text: `Updated core memory: ${params.id}` }],
|
|
100
105
|
details: { id: params.id },
|
|
@@ -106,6 +111,9 @@ export function createCoreMemoryToolDef(state: GlobalPluginState, session: Sessi
|
|
|
106
111
|
return { content: [{ type: "text" as const, text: "Error: 'id' is required for deactivate action." }], details: null };
|
|
107
112
|
}
|
|
108
113
|
await store.deleteCoreMemory(params.id);
|
|
114
|
+
// Invalidate both tiers so removal is reflected next turn
|
|
115
|
+
session.injectedSections.delete("tier0");
|
|
116
|
+
session.injectedSections.delete("tier1");
|
|
109
117
|
return {
|
|
110
118
|
content: [{ type: "text" as const, text: `Deactivated core memory: ${params.id}` }],
|
|
111
119
|
details: { id: params.id },
|
package/src/tools/recall.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function createRecallToolDef(state: GlobalPluginState, session: SessionSt
|
|
|
34
34
|
return { content: [{ type: "text" as const, text: "Memory system unavailable." }], details: null };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const maxResults = Math.min(params.limit ??
|
|
37
|
+
const maxResults = Math.min(params.limit ?? 3, 15);
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
40
|
const queryVec = await embeddings.embed(params.query);
|
|
@@ -63,7 +63,7 @@ export function createRecallToolDef(state: GlobalPluginState, session: SessionSt
|
|
|
63
63
|
|
|
64
64
|
const topIds = results
|
|
65
65
|
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
66
|
-
.slice(0,
|
|
66
|
+
.slice(0, Math.min(maxResults, 8))
|
|
67
67
|
.map((r) => r.id);
|
|
68
68
|
|
|
69
69
|
let neighbors: VectorSearchResult[] = [];
|
|
@@ -87,7 +87,7 @@ export function createRecallToolDef(state: GlobalPluginState, session: SessionSt
|
|
|
87
87
|
const tag = r.table === "turn" ? `[${r.role ?? "turn"}]` : `[${r.table}]`;
|
|
88
88
|
const time = r.timestamp ? ` (${new Date(r.timestamp).toLocaleDateString()})` : "";
|
|
89
89
|
const score = r.score ? ` score:${r.score.toFixed(2)}` : "";
|
|
90
|
-
return `${i + 1}. ${tag}${time}${score}\n ${(r.text ?? "").slice(0,
|
|
90
|
+
return `${i + 1}. ${tag}${time}${score}\n ${(r.text ?? "").slice(0, 300)}`;
|
|
91
91
|
}).join("\n\n");
|
|
92
92
|
|
|
93
93
|
return {
|