@unblocklabs/unblock-memory 0.3.22 → 0.3.24
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 +86 -967
- package/dist/src/config.js +2 -2
- package/dist/src/contracts.d.ts +5 -3
- package/dist/src/diagnostics.d.ts +31 -4
- package/dist/src/diagnostics.js +13 -3
- package/dist/src/manager.d.ts +27 -1
- package/dist/src/manager.js +42 -7
- package/dist/src/memory-whisperer.js +24 -10
- package/dist/src/people-store.d.ts +37 -3
- package/dist/src/people-store.js +23 -9
- package/dist/src/people-tools.js +5 -5
- package/dist/src/plugin.js +31 -35
- package/dist/src/retrieval-telemetry.d.ts +39 -0
- package/dist/src/retrieval-telemetry.js +40 -0
- package/dist/src/session-projector.d.ts +32 -1
- package/dist/src/session-projector.js +84 -12
- package/dist/src/session-sync.d.ts +3 -2
- package/dist/src/session-sync.js +7 -5
- package/dist/src/slack-directory.js +3 -2
- package/dist/src/typesafe-review.d.ts +1 -2
- package/dist/src/typesafe-review.js +3 -11
- package/dist/src/typesafe-transport.d.ts +10 -0
- package/dist/src/typesafe-transport.js +26 -0
- package/dist/src/typesafe.d.ts +1 -1
- package/dist/src/typesafe.js +27 -62
- package/docs/configuration.md +381 -0
- package/docs/peoplesql.md +223 -0
- package/docs/response-audit.md +217 -0
- package/docs/retrieval.md +607 -0
- package/openclaw.plugin.json +10 -8
- package/package.json +7 -2
- package/skills/memory-curator/SKILL.md +5 -0
- package/skills/people-whisperer/SKILL.md +10 -0
package/dist/src/plugin.js
CHANGED
|
@@ -14,28 +14,33 @@ import { getContext } from "./tool-context.js";
|
|
|
14
14
|
import { WhispererDiagnostics } from "./diagnostics.js";
|
|
15
15
|
import { registerReviewTools } from "./review-tools.js";
|
|
16
16
|
import { registerResponseAudit } from "./response-runtime.js";
|
|
17
|
+
import { resolveTimezone } from "./session-projector.js";
|
|
17
18
|
const searchParameters = Type.Object({
|
|
18
19
|
query: Type.String({ pattern: "\\S" }),
|
|
19
|
-
corpora: Type.Optional(Type.Array(Type.String({ pattern: "\\S" }), {
|
|
20
|
+
corpora: Type.Optional(Type.Array(Type.String({ pattern: "\\S" }), {
|
|
21
|
+
minItems: 1, description: 'Configured corpus names; default is all non-skill corpora. Use ["all"] alone for explicit all-corpora recall.',
|
|
22
|
+
})),
|
|
20
23
|
sessionFilter: Type.Optional(Type.Object({
|
|
21
24
|
startedFrom: Type.Optional(Type.String({
|
|
25
|
+
description: "Inclusive lower bound on session start time, not message or claim dates (ISO 8601).",
|
|
22
26
|
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$",
|
|
23
27
|
})),
|
|
24
28
|
startedTo: Type.Optional(Type.String({
|
|
29
|
+
description: "Inclusive upper bound on session start time, not message or claim dates (ISO 8601).",
|
|
25
30
|
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$",
|
|
26
31
|
})),
|
|
27
32
|
provider: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
28
33
|
chatType: Type.Optional(Type.Union([Type.Literal("channel"), Type.Literal("group"), Type.Literal("direct")])),
|
|
29
34
|
accountId: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
30
35
|
conversationId: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
31
|
-
}, { additionalProperties: false })),
|
|
32
|
-
maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
|
|
33
|
-
minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
36
|
+
}, { additionalProperties: false, description: "Restricts session documents only; selected file corpora remain eligible. Not an audience access control." })),
|
|
37
|
+
maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Maximum hits; default 5." })),
|
|
38
|
+
minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1, description: "Minimum vector similarity; default 0.3. Not confidence in factual truth." })),
|
|
34
39
|
}, { additionalProperties: false });
|
|
35
40
|
const getParameters = Type.Object({
|
|
36
41
|
path: Type.String({ pattern: "\\S" }),
|
|
37
|
-
from: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
38
|
-
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
|
|
42
|
+
from: Type.Optional(Type.Integer({ minimum: 1, description: "First source line, 1-based; default 1. Use nextFrom from a truncated read to continue." })),
|
|
43
|
+
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000, description: "Requested lines; default 120, also bounded by 12,000 content characters." })),
|
|
39
44
|
}, { additionalProperties: false });
|
|
40
45
|
const syncSessionsParameters = Type.Object({
|
|
41
46
|
force: Type.Optional(Type.Boolean()),
|
|
@@ -48,7 +53,7 @@ function createSearchTool(runtime, ctx) {
|
|
|
48
53
|
return {
|
|
49
54
|
name: "memory_search",
|
|
50
55
|
label: "Memory Search",
|
|
51
|
-
description: "Search configured memory corpora with
|
|
56
|
+
description: "Search this agent's configured memory corpora with local vector retrieval, not QMD's hybrid query. Skills are excluded. Session snippets are arrays of messages (type, name, timestamp, body; partial when incomplete); file snippets are strings. Results are evidence leads; inspect source context with memory_get. Empty results or errors do not prove absence of a fact.",
|
|
52
57
|
parameters: searchParameters,
|
|
53
58
|
async execute(_toolCallId, params, signal) {
|
|
54
59
|
const { query: untrimmedQuery, corpora, sessionFilter, maxResults, minScore, } = Value.Parse(searchParameters, params);
|
|
@@ -64,18 +69,22 @@ function createSearchTool(runtime, ctx) {
|
|
|
64
69
|
signal,
|
|
65
70
|
requestContext: active.requestContext,
|
|
66
71
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
:
|
|
77
|
-
|
|
78
|
-
}
|
|
72
|
+
// Compact only the public tool response; internal ranking and consumers keep
|
|
73
|
+
// full-precision scores and the host's source/citation compatibility fields.
|
|
74
|
+
const payload = {
|
|
75
|
+
results: results.map(({ source: _source, citation: _citation, session, sessionMessages, ...result }) => ({
|
|
76
|
+
...result,
|
|
77
|
+
snippet: result.corpus === "sessions" ? sessionMessages ?? [{ body: result.snippet, partial: true }] : result.snippet,
|
|
78
|
+
score: Number(result.score.toFixed(2)),
|
|
79
|
+
...(result.vectorScore !== undefined ? { vectorScore: Number(result.vectorScore.toFixed(2)) } : {}),
|
|
80
|
+
...(result.textScore !== undefined ? { textScore: Number(result.textScore.toFixed(2)) } : {}),
|
|
81
|
+
...(session ? { session: { ...session, startedAt: new Date(session.startedAt).toISOString() } } : {}),
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
86
|
+
details: payload,
|
|
87
|
+
};
|
|
79
88
|
},
|
|
80
89
|
};
|
|
81
90
|
}
|
|
@@ -86,7 +95,7 @@ function createGetTool(runtime, ctx) {
|
|
|
86
95
|
return {
|
|
87
96
|
name: "memory_get",
|
|
88
97
|
label: "Memory Get",
|
|
89
|
-
description: "Read an exact qmd:// path returned by
|
|
98
|
+
description: "Read an exact indexed qmd:// source path returned by memory tools. Defaults to 120 lines, bounded to 12,000 content characters. Check truncated/nextFrom and continue when present; not_found or unavailable is not a successful empty read.",
|
|
90
99
|
parameters: getParameters,
|
|
91
100
|
async execute(_toolCallId, params) {
|
|
92
101
|
const { path: untrimmedPath, from, lines } = Value.Parse(getParameters, params);
|
|
@@ -383,25 +392,12 @@ function parseByteSize(value) {
|
|
|
383
392
|
const bytes = Math.round(Number(match[1]) * 1024 ** powers[unit]);
|
|
384
393
|
return Number.isSafeInteger(bytes) ? bytes : undefined;
|
|
385
394
|
}
|
|
386
|
-
function resolveTimezone(cfg) {
|
|
387
|
-
const configured = cfg?.agents?.defaults?.userTimezone?.trim();
|
|
388
|
-
if (configured) {
|
|
389
|
-
try {
|
|
390
|
-
new Intl.DateTimeFormat("en-US", { timeZone: configured }).format();
|
|
391
|
-
return configured;
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
// Host validation normally prevents this; fall through defensively.
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
398
|
-
}
|
|
399
395
|
export function resolveFlushPlan(params = {}) {
|
|
400
396
|
const configured = params.cfg?.agents?.defaults?.compaction?.memoryFlush;
|
|
401
397
|
if (configured?.enabled === false)
|
|
402
398
|
return null;
|
|
403
399
|
const nowMs = params.nowMs ?? Date.now();
|
|
404
|
-
const date = formatDateInTimezone(nowMs, resolveTimezone(params.cfg));
|
|
400
|
+
const date = formatDateInTimezone(nowMs, resolveTimezone(params.cfg?.agents?.defaults?.userTimezone?.trim()));
|
|
405
401
|
const target = `memory/${date}.md`;
|
|
406
402
|
return {
|
|
407
403
|
softThresholdTokens: nonNegativeInteger(configured?.softThresholdTokens, 4000),
|
|
@@ -427,7 +423,7 @@ export function registerUnblockMemory(api) {
|
|
|
427
423
|
supportsPrivateTranscriptRecall: false,
|
|
428
424
|
promptBuilder: ({ availableTools }) => availableTools.has("memory_search")
|
|
429
425
|
? [
|
|
430
|
-
"Use memory_search for relevant past facts, then memory_get
|
|
426
|
+
"Use memory_search for relevant past facts, then memory_get to verify source context, attribution and dates. Follow read continuation when present. Empty search is not proof of absence; historical memory is not current authorization.",
|
|
431
427
|
]
|
|
432
428
|
: [],
|
|
433
429
|
flushPlanResolver: resolveFlushPlan,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type RetrievalOperation = "vector" | "lexical" | "memoryWhisperer";
|
|
2
|
+
type RetrievalOutcome = "ok" | "empty" | "failed" | "cancelled" | "timed_out" | "skipped";
|
|
3
|
+
declare const fields: readonly ["elapsedMs", "retrievalMs", "judgeMs", "candidates", "eligible", "results", "contextChars"];
|
|
4
|
+
type Field = typeof fields[number];
|
|
5
|
+
export type RetrievalObservation = {
|
|
6
|
+
outcome: RetrievalOutcome;
|
|
7
|
+
elapsedMs: number;
|
|
8
|
+
} & Partial<Record<Field, number>>;
|
|
9
|
+
/** Only fixed operation/outcome names and nonnegative numbers enter this store. */
|
|
10
|
+
export declare class RetrievalTelemetry {
|
|
11
|
+
#private;
|
|
12
|
+
record(operation: RetrievalOperation, observation: RetrievalObservation): void;
|
|
13
|
+
snapshot(): {
|
|
14
|
+
scope: string;
|
|
15
|
+
operations: {
|
|
16
|
+
[k: string]: {
|
|
17
|
+
calls: number;
|
|
18
|
+
outcomes: {
|
|
19
|
+
ok?: number | undefined;
|
|
20
|
+
skipped?: number | undefined;
|
|
21
|
+
failed?: number | undefined;
|
|
22
|
+
empty?: number | undefined;
|
|
23
|
+
cancelled?: number | undefined;
|
|
24
|
+
timed_out?: number | undefined;
|
|
25
|
+
};
|
|
26
|
+
measurements: {
|
|
27
|
+
[k: string]: {
|
|
28
|
+
total: number;
|
|
29
|
+
samples: number;
|
|
30
|
+
recentSamples: number;
|
|
31
|
+
p50: number | null;
|
|
32
|
+
p95: number | null;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const fields = ["elapsedMs", "retrievalMs", "judgeMs", "candidates", "eligible", "results", "contextChars"];
|
|
2
|
+
const recentLimit = 256;
|
|
3
|
+
const boundedAdd = (left, right) => Math.min(Number.MAX_SAFE_INTEGER, left + right);
|
|
4
|
+
/** Only fixed operation/outcome names and nonnegative numbers enter this store. */
|
|
5
|
+
export class RetrievalTelemetry {
|
|
6
|
+
#entries = new Map();
|
|
7
|
+
record(operation, observation) {
|
|
8
|
+
const entry = this.#entries.get(operation) ?? { calls: 0, outcomes: {}, totals: {}, recent: [] };
|
|
9
|
+
this.#entries.set(operation, entry);
|
|
10
|
+
entry.calls = boundedAdd(entry.calls, 1);
|
|
11
|
+
entry.outcomes[observation.outcome] = boundedAdd(entry.outcomes[observation.outcome] ?? 0, 1);
|
|
12
|
+
const sample = {};
|
|
13
|
+
for (const field of fields) {
|
|
14
|
+
const value = observation[field];
|
|
15
|
+
if (value === undefined || !Number.isFinite(value) || value < 0)
|
|
16
|
+
continue;
|
|
17
|
+
sample[field] = value;
|
|
18
|
+
const total = entry.totals[field] ?? { sum: 0, samples: 0 };
|
|
19
|
+
entry.totals[field] = { sum: boundedAdd(total.sum, value), samples: boundedAdd(total.samples, 1) };
|
|
20
|
+
}
|
|
21
|
+
entry.recent.push(sample);
|
|
22
|
+
if (entry.recent.length > recentLimit)
|
|
23
|
+
entry.recent.shift();
|
|
24
|
+
}
|
|
25
|
+
snapshot() {
|
|
26
|
+
return {
|
|
27
|
+
scope: "Lifetime counters; percentiles cover at most the last 256 calls per operation, including failures. No content or hashes; resets on recreation.",
|
|
28
|
+
operations: Object.fromEntries([...this.#entries].map(([operation, entry]) => [operation, {
|
|
29
|
+
calls: entry.calls,
|
|
30
|
+
outcomes: { ...entry.outcomes },
|
|
31
|
+
measurements: Object.fromEntries(fields.map(field => {
|
|
32
|
+
const values = entry.recent.flatMap(sample => sample[field] === undefined ? [] : [sample[field]]).sort((a, b) => a - b);
|
|
33
|
+
const percentile = (fraction) => values.length ? values[Math.ceil(values.length * fraction) - 1] : null;
|
|
34
|
+
return [field, { total: entry.totals[field]?.sum ?? 0, samples: entry.totals[field]?.samples ?? 0,
|
|
35
|
+
recentSamples: values.length, p50: percentile(0.5), p95: percentile(0.95) }];
|
|
36
|
+
})),
|
|
37
|
+
}])),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -22,10 +22,27 @@ export type SessionProjectionInput = SessionMetadata & {
|
|
|
22
22
|
attachmentBudgetSkipped: number;
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
|
+
export type SessionSnippetMessage = {
|
|
26
|
+
type?: "user" | "assistant";
|
|
27
|
+
name?: string;
|
|
28
|
+
timestamp?: string;
|
|
29
|
+
body: string;
|
|
30
|
+
partial?: true;
|
|
31
|
+
};
|
|
32
|
+
/** Character offsets in the exact indexed projection; end excludes message separators. */
|
|
33
|
+
export type SessionMessageSpan = {
|
|
34
|
+
type: "user" | "assistant";
|
|
35
|
+
name: string;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
start: number;
|
|
38
|
+
bodyStart: number;
|
|
39
|
+
end: number;
|
|
40
|
+
};
|
|
25
41
|
export type SessionContextSpans = {
|
|
26
42
|
message: {
|
|
27
43
|
start: number;
|
|
28
44
|
end: number;
|
|
45
|
+
timestamp: string;
|
|
29
46
|
};
|
|
30
47
|
turn: {
|
|
31
48
|
start: number;
|
|
@@ -33,6 +50,20 @@ export type SessionContextSpans = {
|
|
|
33
50
|
};
|
|
34
51
|
};
|
|
35
52
|
export declare function projectSession(input: SessionProjectionInput): string | undefined;
|
|
36
|
-
export declare function
|
|
53
|
+
export declare function projectSessionDocument(input: SessionProjectionInput): {
|
|
54
|
+
content: string;
|
|
55
|
+
messages: SessionMessageSpan[];
|
|
56
|
+
} | undefined;
|
|
57
|
+
/** Legacy fallback only. New projections retain exact boundaries before rendering Markdown. */
|
|
58
|
+
export declare function parseSessionMessageSpans(content: string): SessionMessageSpan[];
|
|
59
|
+
export declare function sessionContextSpans(content: string, position: number, markers?: SessionMessageSpan[]): SessionContextSpans | undefined;
|
|
60
|
+
export declare function sessionSnippetMessages(content: string, selected: {
|
|
61
|
+
text: string;
|
|
62
|
+
position: number;
|
|
63
|
+
sourceText?: string;
|
|
64
|
+
}, spans: readonly SessionMessageSpan[], identity?: {
|
|
65
|
+
agentId: string;
|
|
66
|
+
agentName: string;
|
|
67
|
+
}): SessionSnippetMessage[];
|
|
37
68
|
export declare function sessionDocumentPath(metadata: SessionMetadata): string;
|
|
38
69
|
export declare function resolveTimezone(configured?: string): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { projectLoggieMessage } from "./loggie-projection.js";
|
|
3
3
|
import { applyProposal, parseAttachments, parseInternalMessage } from "./session-noise.js";
|
|
4
|
-
const MESSAGE_HEADING = /^## (User|Assistant) —
|
|
4
|
+
const MESSAGE_HEADING = /^## (User|Assistant) — (.+) — (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \S.*)$/u;
|
|
5
5
|
function record(value) {
|
|
6
6
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
7
7
|
? value
|
|
@@ -126,6 +126,9 @@ function formatTimestamp(value, timezone) {
|
|
|
126
126
|
`${part("hour")}:${part("minute")}:${part("second")} ${part("timeZoneName")}`.trim();
|
|
127
127
|
}
|
|
128
128
|
export function projectSession(input) {
|
|
129
|
+
return projectSessionDocument(input)?.content;
|
|
130
|
+
}
|
|
131
|
+
export function projectSessionDocument(input) {
|
|
129
132
|
const messages = input.events.flatMap((event) => {
|
|
130
133
|
const projected = projectMessage(event, input);
|
|
131
134
|
return projected ? [projected] : [];
|
|
@@ -164,28 +167,64 @@ export function projectSession(input) {
|
|
|
164
167
|
else if (!previous || (!previous.meeting?.complete && meeting.complete))
|
|
165
168
|
latest.set(meeting.key, message);
|
|
166
169
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
let content = "# Transcript\n\n";
|
|
171
|
+
const spans = [];
|
|
172
|
+
for (const message of messages.filter(message => !hidden.has(message))) {
|
|
173
|
+
if (spans.length)
|
|
174
|
+
content += "\n\n";
|
|
175
|
+
const start = content.length;
|
|
176
|
+
const timestamp = formatTimestamp(message.timestamp, input.timezone);
|
|
177
|
+
content += `## ${message.role === "user" ? "User" : "Assistant"} — ${message.speaker} — ${timestamp}\n\n`;
|
|
178
|
+
const bodyStart = content.length;
|
|
179
|
+
content += message.text;
|
|
180
|
+
spans.push({ type: message.role, name: message.speaker, timestamp, start, bodyStart, end: content.length });
|
|
181
|
+
}
|
|
182
|
+
return { content: `${content}\n`, messages: spans };
|
|
170
183
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
/** Legacy fallback only. New projections retain exact boundaries before rendering Markdown. */
|
|
185
|
+
export function parseSessionMessageSpans(content) {
|
|
186
|
+
const messages = [];
|
|
187
|
+
let fence;
|
|
188
|
+
for (const line of content.matchAll(/[^\n]*(?:\n|$)/gu)) {
|
|
189
|
+
const text = line[0].replace(/\n$/u, "");
|
|
190
|
+
const delimiter = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(text);
|
|
191
|
+
if (fence) {
|
|
192
|
+
if (delimiter?.[1]?.[0] === fence.char && delimiter[1].length >= fence.length && !delimiter[2]?.trim())
|
|
193
|
+
fence = undefined;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (delimiter) {
|
|
197
|
+
fence = { char: delimiter[1][0], length: delimiter[1].length };
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const match = MESSAGE_HEADING.exec(text);
|
|
201
|
+
// Only the projector's complete heading + blank-line form is recognized.
|
|
202
|
+
if (!match || !content.startsWith("\n\n", line.index + text.length))
|
|
203
|
+
continue;
|
|
204
|
+
const previous = messages.at(-1);
|
|
205
|
+
if (previous)
|
|
206
|
+
previous.end = content.startsWith("\n\n", line.index - 2) ? line.index - 2 : line.index;
|
|
207
|
+
messages.push({ type: match[1] === "User" ? "user" : "assistant", name: match[2], timestamp: match[3],
|
|
208
|
+
start: line.index, bodyStart: line.index + text.length + 2,
|
|
209
|
+
end: content.endsWith("\n") ? content.length - 1 : content.length });
|
|
210
|
+
}
|
|
211
|
+
return messages;
|
|
212
|
+
}
|
|
213
|
+
export function sessionContextSpans(content, position, markers = parseSessionMessageSpans(content)) {
|
|
176
214
|
const containing = markers.findLastIndex((marker) => marker.start <= position);
|
|
177
215
|
if (containing < 0)
|
|
178
216
|
return undefined;
|
|
179
217
|
const message = {
|
|
180
218
|
start: markers[containing].start,
|
|
181
219
|
end: markers[containing + 1]?.start ?? content.length,
|
|
220
|
+
timestamp: markers[containing].timestamp,
|
|
182
221
|
};
|
|
183
222
|
let turnStart = containing;
|
|
184
|
-
while (turnStart > 0 && markers[turnStart].
|
|
223
|
+
while (turnStart > 0 && markers[turnStart].type !== "user")
|
|
185
224
|
turnStart -= 1;
|
|
186
|
-
if (markers[turnStart].
|
|
225
|
+
if (markers[turnStart].type !== "user")
|
|
187
226
|
turnStart = containing;
|
|
188
|
-
const nextUser = markers.findIndex((marker, index) => index > turnStart && marker.
|
|
227
|
+
const nextUser = markers.findIndex((marker, index) => index > turnStart && marker.type === "user");
|
|
189
228
|
return {
|
|
190
229
|
message,
|
|
191
230
|
turn: {
|
|
@@ -194,6 +233,39 @@ export function sessionContextSpans(content, position) {
|
|
|
194
233
|
},
|
|
195
234
|
};
|
|
196
235
|
}
|
|
236
|
+
export function sessionSnippetMessages(content, selected, spans, identity) {
|
|
237
|
+
const sourceText = selected.sourceText ?? selected.text;
|
|
238
|
+
const end = selected.position + sourceText.length;
|
|
239
|
+
// Added meeting speaker/revision context is evidence too; retain it in the first body.
|
|
240
|
+
const prefix = selected.text.endsWith(sourceText) ? selected.text.slice(0, selected.text.length - sourceText.length) : "";
|
|
241
|
+
const messages = [];
|
|
242
|
+
let cursor = selected.position;
|
|
243
|
+
const keepUnattributed = (from, to) => {
|
|
244
|
+
const body = content.slice(from, to);
|
|
245
|
+
if (body.trim() && !(from === 0 && body === "# Transcript\n\n"))
|
|
246
|
+
messages.push({ body, partial: true });
|
|
247
|
+
};
|
|
248
|
+
for (const span of spans) {
|
|
249
|
+
if (span.start >= end || span.end <= selected.position)
|
|
250
|
+
continue;
|
|
251
|
+
if (span.start > cursor)
|
|
252
|
+
keepUnattributed(cursor, span.start);
|
|
253
|
+
const from = Math.max(span.bodyStart, selected.position);
|
|
254
|
+
const to = Math.min(span.end, end);
|
|
255
|
+
messages.push({ type: span.type,
|
|
256
|
+
name: span.type === "assistant" && span.name === identity?.agentId ? identity.agentName : span.name,
|
|
257
|
+
timestamp: span.timestamp, body: content.slice(from, Math.max(from, to)),
|
|
258
|
+
...(from > span.bodyStart || to < span.end ? { partial: true } : {}),
|
|
259
|
+
});
|
|
260
|
+
cursor = Math.min(span.end, end);
|
|
261
|
+
}
|
|
262
|
+
if (cursor < end)
|
|
263
|
+
keepUnattributed(cursor, end);
|
|
264
|
+
if (!messages.length)
|
|
265
|
+
return [{ body: selected.text, partial: true }];
|
|
266
|
+
messages[0].body = prefix + messages[0].body;
|
|
267
|
+
return messages;
|
|
268
|
+
}
|
|
197
269
|
function hash(value) {
|
|
198
270
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
199
271
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChatType } from "./config.js";
|
|
2
|
-
import { type SessionMetadata, type SessionProjectionInput } from "./session-projector.js";
|
|
3
|
-
export declare const PROJECTOR_VERSION =
|
|
2
|
+
import { type SessionMetadata, type SessionProjectionInput, type SessionMessageSpan } from "./session-projector.js";
|
|
3
|
+
export declare const PROJECTOR_VERSION = 7;
|
|
4
4
|
type IndexedSession = SessionMetadata & {
|
|
5
5
|
sourceGeneration: string;
|
|
6
6
|
maxSeq: number;
|
|
@@ -10,6 +10,7 @@ type IndexedSession = SessionMetadata & {
|
|
|
10
10
|
documentPath: string;
|
|
11
11
|
projectorVersion: number;
|
|
12
12
|
sourceFingerprint?: string;
|
|
13
|
+
messages?: SessionMessageSpan[];
|
|
13
14
|
};
|
|
14
15
|
export type SessionManifest = {
|
|
15
16
|
version: number;
|
package/dist/src/session-sync.js
CHANGED
|
@@ -3,9 +3,9 @@ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { chmod, mkdir, readFile, rename, unlink, utimes, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
|
-
import {
|
|
6
|
+
import { projectSessionDocument, sessionDocumentPath, } from "./session-projector.js";
|
|
7
7
|
const MANIFEST_VERSION = 1;
|
|
8
|
-
export const PROJECTOR_VERSION =
|
|
8
|
+
export const PROJECTOR_VERSION = 7;
|
|
9
9
|
const SUPPORTED_SCHEMA_VERSIONS = new Set([17, 18, 19]);
|
|
10
10
|
// Source lives in src/, published code in dist/src/. Read our own pinned dependency
|
|
11
11
|
// metadata, not QMD internals (which may also be substituted by runtime inspectors).
|
|
@@ -290,7 +290,7 @@ export async function syncSessionProjections(params) {
|
|
|
290
290
|
sessions[window.sessionId] = previous;
|
|
291
291
|
continue;
|
|
292
292
|
}
|
|
293
|
-
let
|
|
293
|
+
let projection;
|
|
294
294
|
try {
|
|
295
295
|
const input = {
|
|
296
296
|
...metadata,
|
|
@@ -300,7 +300,7 @@ export async function syncSessionProjections(params) {
|
|
|
300
300
|
events,
|
|
301
301
|
diagnostics,
|
|
302
302
|
};
|
|
303
|
-
|
|
303
|
+
projection = projectSessionDocument(input);
|
|
304
304
|
}
|
|
305
305
|
catch {
|
|
306
306
|
counts.failed += 1;
|
|
@@ -308,7 +308,7 @@ export async function syncSessionProjections(params) {
|
|
|
308
308
|
sessions[window.sessionId] = previous;
|
|
309
309
|
continue;
|
|
310
310
|
}
|
|
311
|
-
if (!
|
|
311
|
+
if (!projection) {
|
|
312
312
|
ignoredSessions[window.sessionId] = JSON.stringify(window);
|
|
313
313
|
counts.skipped += 1;
|
|
314
314
|
if (previous) {
|
|
@@ -317,6 +317,7 @@ export async function syncSessionProjections(params) {
|
|
|
317
317
|
}
|
|
318
318
|
continue;
|
|
319
319
|
}
|
|
320
|
+
const { content, messages } = projection;
|
|
320
321
|
const target = projectionPath(params.outputDir, documentPath);
|
|
321
322
|
const hash = projectionHash(content);
|
|
322
323
|
const contentChanged = params.force === true || previous?.projectorVersion !== PROJECTOR_VERSION ||
|
|
@@ -338,6 +339,7 @@ export async function syncSessionProjections(params) {
|
|
|
338
339
|
documentPath,
|
|
339
340
|
projectorVersion: PROJECTOR_VERSION,
|
|
340
341
|
sourceFingerprint: JSON.stringify(window),
|
|
342
|
+
messages,
|
|
341
343
|
};
|
|
342
344
|
if (contentChanged)
|
|
343
345
|
counts.updated += 1;
|
|
@@ -95,7 +95,8 @@ export async function syncSlackDirectory(params) {
|
|
|
95
95
|
}
|
|
96
96
|
try {
|
|
97
97
|
const existing = params.store.findIdentity("slack", params.accountId, externalId);
|
|
98
|
-
|
|
98
|
+
const existingPerson = existing ? params.store.getPerson(existing.personId) : undefined;
|
|
99
|
+
if (existing && existingPerson?.status !== "active") {
|
|
99
100
|
counts.skipped += 1;
|
|
100
101
|
continue;
|
|
101
102
|
}
|
|
@@ -118,7 +119,7 @@ export async function syncSlackDirectory(params) {
|
|
|
118
119
|
});
|
|
119
120
|
if (result.created)
|
|
120
121
|
counts.created += 1;
|
|
121
|
-
else if (changed)
|
|
122
|
+
else if (changed || result.person.displayName !== existingPerson?.displayName)
|
|
122
123
|
counts.updated += 1;
|
|
123
124
|
else
|
|
124
125
|
counts.unchanged += 1;
|
|
@@ -6,7 +6,7 @@ type RequestOptions = {
|
|
|
6
6
|
type Json = string | number | boolean | null | Json[] | {
|
|
7
7
|
[key: string]: Json;
|
|
8
8
|
};
|
|
9
|
-
export
|
|
9
|
+
export { TYPESAFE_MODEL as TYPESAFE_REVIEW_MODEL } from "./typesafe-transport.js";
|
|
10
10
|
export declare function askTypeSafeReview(params: RequestOptions, state: Json, questions: Json): Promise<unknown>;
|
|
11
11
|
/** The source is an indexed snapshot, not proof of current truth or permission to write. */
|
|
12
12
|
export declare function reviewTypeSafeClaim(params: RequestOptions & {
|
|
@@ -50,4 +50,3 @@ export declare function reviewClusterDefects(params: RequestOptions & {
|
|
|
50
50
|
defect: "encoding" | "wrapper" | "boilerplate" | "none_or_uncertain";
|
|
51
51
|
confidence: number;
|
|
52
52
|
}[]>;
|
|
53
|
-
export {};
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { backgroundWordCount, PEOPLE_BACKGROUND_MAX_WORDS } from "./people-background.js";
|
|
4
|
-
|
|
4
|
+
import { postTypeSafe } from "./typesafe-transport.js";
|
|
5
|
+
export { TYPESAFE_MODEL as TYPESAFE_REVIEW_MODEL } from "./typesafe-transport.js";
|
|
5
6
|
export async function askTypeSafeReview(params, state, questions) {
|
|
6
7
|
const signal = AbortSignal.any([params.signal, AbortSignal.timeout(params.timeoutMs)]);
|
|
7
8
|
try {
|
|
8
9
|
signal.throwIfAborted();
|
|
9
|
-
|
|
10
|
-
method: "POST", redirect: "error", signal,
|
|
11
|
-
headers: { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json" },
|
|
12
|
-
body: JSON.stringify({ model: TYPESAFE_REVIEW_MODEL, state, questions }),
|
|
13
|
-
});
|
|
14
|
-
if (!response.ok) {
|
|
15
|
-
await response.body?.cancel();
|
|
16
|
-
throw new Error("HTTP failure");
|
|
17
|
-
}
|
|
18
|
-
return await response.json();
|
|
10
|
+
return await postTypeSafe({ apiKey: params.apiKey, signal }, state, questions);
|
|
19
11
|
}
|
|
20
12
|
catch {
|
|
21
13
|
throw new Error(signal.aborted ? "TypeSafe review aborted" : "TypeSafe review unavailable");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const TYPESAFE_MODEL = "jev-1.13.0";
|
|
2
|
+
export declare class TypeSafeHttpError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
constructor(status: number);
|
|
5
|
+
}
|
|
6
|
+
/** Shared wire protocol; callers own deadlines, judgments and public errors. */
|
|
7
|
+
export declare function postTypeSafe(params: {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
}, state: unknown, questions: unknown): Promise<unknown>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const TYPESAFE_MODEL = "jev-1.13.0";
|
|
2
|
+
export class TypeSafeHttpError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(status) {
|
|
5
|
+
super(`TypeSafe HTTP ${status}`);
|
|
6
|
+
this.status = status;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Shared wire protocol; callers own deadlines, judgments and public errors. */
|
|
10
|
+
export async function postTypeSafe(params, state, questions) {
|
|
11
|
+
const response = await fetch("https://api.typesafe.ai/v1/systemone", {
|
|
12
|
+
method: "POST", redirect: "error", signal: params.signal,
|
|
13
|
+
headers: { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json" },
|
|
14
|
+
body: JSON.stringify({ model: TYPESAFE_MODEL, state, questions }),
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
try {
|
|
18
|
+
await response.body?.cancel();
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
// Preserve the status even if cancellation fails; never include provider content.
|
|
22
|
+
throw new TypeSafeHttpError(response.status);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return response.json();
|
|
26
|
+
}
|
package/dist/src/typesafe.d.ts
CHANGED