@wfm-cn/dsh-storage 0.0.0-stage → 0.3.0
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 +23 -0
- package/LICENSE +21 -0
- package/README.md +137 -2
- package/cordis.patch.yml +3 -0
- package/lib/backends/database.js +578 -0
- package/lib/backfill.js +331 -0
- package/lib/client/index.js +79 -0
- package/lib/client/panel.js +299 -0
- package/lib/client/toast.js +66 -0
- package/lib/client.js +1 -0
- package/lib/compaction.js +108 -0
- package/lib/config.js +171 -0
- package/lib/ddl.js +323 -0
- package/lib/generated/mysql/browser.js +17 -0
- package/lib/generated/mysql/client.js +34 -0
- package/lib/generated/mysql/commonInputTypes.js +10 -0
- package/lib/generated/mysql/enums.js +10 -0
- package/lib/generated/mysql/internal/class.js +49 -0
- package/lib/generated/mysql/internal/prismaNamespace.js +146 -0
- package/lib/generated/mysql/internal/prismaNamespaceBrowser.js +117 -0
- package/lib/generated/mysql/models/AiChatHistory.js +1 -0
- package/lib/generated/mysql/models/AiMessage.js +1 -0
- package/lib/generated/mysql/models.js +1 -0
- package/lib/generated/postgresql/browser.js +17 -0
- package/lib/generated/postgresql/client.js +34 -0
- package/lib/generated/postgresql/commonInputTypes.js +10 -0
- package/lib/generated/postgresql/enums.js +10 -0
- package/lib/generated/postgresql/internal/class.js +49 -0
- package/lib/generated/postgresql/internal/prismaNamespace.js +131 -0
- package/lib/generated/postgresql/internal/prismaNamespaceBrowser.js +102 -0
- package/lib/generated/postgresql/models/AiChatHistory.js +1 -0
- package/lib/generated/postgresql/models/AiMessage.js +1 -0
- package/lib/generated/postgresql/models.js +1 -0
- package/lib/generated/sqlite/browser.js +17 -0
- package/lib/generated/sqlite/client.js +34 -0
- package/lib/generated/sqlite/commonInputTypes.js +10 -0
- package/lib/generated/sqlite/enums.js +10 -0
- package/lib/generated/sqlite/internal/class.js +49 -0
- package/lib/generated/sqlite/internal/prismaNamespace.js +128 -0
- package/lib/generated/sqlite/internal/prismaNamespaceBrowser.js +99 -0
- package/lib/generated/sqlite/models/AiChatHistory.js +1 -0
- package/lib/generated/sqlite/models/AiMessage.js +1 -0
- package/lib/generated/sqlite/models.js +1 -0
- package/lib/generated/sqlserver/browser.js +17 -0
- package/lib/generated/sqlserver/client.js +34 -0
- package/lib/generated/sqlserver/commonInputTypes.js +10 -0
- package/lib/generated/sqlserver/enums.js +10 -0
- package/lib/generated/sqlserver/internal/class.js +49 -0
- package/lib/generated/sqlserver/internal/prismaNamespace.js +119 -0
- package/lib/generated/sqlserver/internal/prismaNamespaceBrowser.js +90 -0
- package/lib/generated/sqlserver/models/AiChatHistory.js +1 -0
- package/lib/generated/sqlserver/models/AiMessage.js +1 -0
- package/lib/generated/sqlserver/models.js +1 -0
- package/lib/health.js +180 -0
- package/lib/index.js +681 -0
- package/lib/memory-tools.js +365 -0
- package/lib/notify.js +56 -0
- package/lib/projector.js +191 -0
- package/lib/status-route.js +172 -0
- package/lib/types/backends/database.d.ts +78 -0
- package/lib/types/backfill.d.ts +67 -0
- package/lib/types/client/index.d.ts +12 -0
- package/lib/types/client/panel.d.ts +79 -0
- package/lib/types/client/toast.d.ts +38 -0
- package/lib/types/compaction.d.ts +3 -0
- package/lib/types/config.d.ts +52 -0
- package/lib/types/ddl.d.ts +49 -0
- package/lib/types/generated/mysql/browser.d.ts +14 -0
- package/lib/types/generated/mysql/client.d.ts +33 -0
- package/lib/types/generated/mysql/commonInputTypes.d.ts +396 -0
- package/lib/types/generated/mysql/enums.d.ts +1 -0
- package/lib/types/generated/mysql/internal/class.d.ts +143 -0
- package/lib/types/generated/mysql/internal/prismaNamespace.d.ts +709 -0
- package/lib/types/generated/mysql/internal/prismaNamespaceBrowser.d.ts +112 -0
- package/lib/types/generated/mysql/models/AiChatHistory.d.ts +1258 -0
- package/lib/types/generated/mysql/models/AiMessage.d.ts +1359 -0
- package/lib/types/generated/mysql/models.d.ts +3 -0
- package/lib/types/generated/postgresql/browser.d.ts +14 -0
- package/lib/types/generated/postgresql/client.d.ts +33 -0
- package/lib/types/generated/postgresql/commonInputTypes.d.ts +392 -0
- package/lib/types/generated/postgresql/enums.d.ts +1 -0
- package/lib/types/generated/postgresql/internal/class.d.ts +143 -0
- package/lib/types/generated/postgresql/internal/prismaNamespace.d.ts +728 -0
- package/lib/types/generated/postgresql/internal/prismaNamespaceBrowser.d.ts +95 -0
- package/lib/types/generated/postgresql/models/AiChatHistory.d.ts +1373 -0
- package/lib/types/generated/postgresql/models/AiMessage.d.ts +1496 -0
- package/lib/types/generated/postgresql/models.d.ts +3 -0
- package/lib/types/generated/sqlite/browser.d.ts +14 -0
- package/lib/types/generated/sqlite/client.d.ts +33 -0
- package/lib/types/generated/sqlite/commonInputTypes.d.ts +373 -0
- package/lib/types/generated/sqlite/enums.d.ts +1 -0
- package/lib/types/generated/sqlite/internal/class.d.ts +143 -0
- package/lib/types/generated/sqlite/internal/prismaNamespace.d.ts +705 -0
- package/lib/types/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +92 -0
- package/lib/types/generated/sqlite/models/AiChatHistory.d.ts +1371 -0
- package/lib/types/generated/sqlite/models/AiMessage.d.ts +1493 -0
- package/lib/types/generated/sqlite/models.d.ts +3 -0
- package/lib/types/generated/sqlserver/browser.d.ts +14 -0
- package/lib/types/generated/sqlserver/client.d.ts +33 -0
- package/lib/types/generated/sqlserver/commonInputTypes.d.ts +333 -0
- package/lib/types/generated/sqlserver/enums.d.ts +1 -0
- package/lib/types/generated/sqlserver/internal/class.d.ts +143 -0
- package/lib/types/generated/sqlserver/internal/prismaNamespace.d.ts +669 -0
- package/lib/types/generated/sqlserver/internal/prismaNamespaceBrowser.d.ts +80 -0
- package/lib/types/generated/sqlserver/models/AiChatHistory.d.ts +1257 -0
- package/lib/types/generated/sqlserver/models/AiMessage.d.ts +1375 -0
- package/lib/types/generated/sqlserver/models.d.ts +3 -0
- package/lib/types/health.d.ts +52 -0
- package/lib/types/index.d.ts +91 -0
- package/lib/types/memory-tools.d.ts +168 -0
- package/lib/types/notify.d.ts +35 -0
- package/lib/types/projector.d.ts +51 -0
- package/lib/types/status-route.d.ts +48 -0
- package/lib/types/types.d.ts +58 -0
- package/lib/types.js +8 -0
- package/package.json +127 -4
- package/preview.png +0 -0
- package/prisma/schema.mysql.prisma +78 -0
- package/prisma/schema.postgresql.prisma +70 -0
- package/prisma/schema.sqlite.prisma +70 -0
- package/prisma/schema.sqlserver.prisma +76 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type SyncConfig } from './config.js';
|
|
2
|
+
export type HealthStatus = {
|
|
3
|
+
state: 'ok' | 'down' | 'disabled' | 'config-error';
|
|
4
|
+
lastOkAt?: number;
|
|
5
|
+
lastError?: string;
|
|
6
|
+
retryCount: number;
|
|
7
|
+
mirroredTotal?: number;
|
|
8
|
+
};
|
|
9
|
+
/** Thrown by the probe when the sync config itself is unusable — no retries until it is fixed. */
|
|
10
|
+
export declare class ConfigError extends Error {
|
|
11
|
+
}
|
|
12
|
+
type TransitionCallback = (to: HealthStatus['state'], from: HealthStatus['state'], status: HealthStatus) => void;
|
|
13
|
+
export interface HealthMonitorOptions {
|
|
14
|
+
/** Re-probe interval while healthy (default 30s). */
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* States: 'disabled' (not started) → 'ok'/'down' on probe results, or
|
|
19
|
+
* 'config-error' when the probe reports a ConfigError (retries suspended
|
|
20
|
+
* until an explicit `recheck()`). Same-state results never re-fire
|
|
21
|
+
* transition callbacks.
|
|
22
|
+
*/
|
|
23
|
+
export declare class HealthMonitor {
|
|
24
|
+
private readonly probe;
|
|
25
|
+
private readonly options;
|
|
26
|
+
private state;
|
|
27
|
+
private lastOkAt;
|
|
28
|
+
private lastError;
|
|
29
|
+
private retryCount;
|
|
30
|
+
private running;
|
|
31
|
+
private timer;
|
|
32
|
+
private readonly listeners;
|
|
33
|
+
constructor(probe: () => Promise<void>, options?: HealthMonitorOptions);
|
|
34
|
+
start(): void;
|
|
35
|
+
stop(): void;
|
|
36
|
+
snapshot(): HealthStatus;
|
|
37
|
+
onTransition(cb: TransitionCallback): () => void;
|
|
38
|
+
/** Run one probe cycle immediately (re-arms after a config error). */
|
|
39
|
+
recheck(): Promise<void>;
|
|
40
|
+
private clearTimer;
|
|
41
|
+
private runProbe;
|
|
42
|
+
private onSuccess;
|
|
43
|
+
private onFailure;
|
|
44
|
+
private scheduleRetry;
|
|
45
|
+
private transition;
|
|
46
|
+
}
|
|
47
|
+
/** Probe for HealthMonitor: resolves when the database is live and migrated. */
|
|
48
|
+
export declare function probeDatabase(cfg: SyncConfig): Promise<void>;
|
|
49
|
+
export type ConnectionTestResult = 'ok' | 'no-tables' | `fail:${string}`;
|
|
50
|
+
/** One-shot panel test: never throws — every failure maps to 'fail:<reason>'. */
|
|
51
|
+
export declare function testConnection(cfg: SyncConfig): Promise<ConnectionTestResult>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-storage — mirrors the dsh session event stream into the ai_messages /
|
|
3
|
+
* ai_chat_histories tables (MySQL/PostgreSQL/SQLite/SQL Server).
|
|
4
|
+
*
|
|
5
|
+
* Design: dsh's own session persistence (JSONL/SQLite) stays authoritative.
|
|
6
|
+
* This plugin taps `session/event` (the same seam the built-in persistence
|
|
7
|
+
* coordinator uses) and projects events into MessageRow / SessionRow shaped
|
|
8
|
+
* after the source project's ai_messages / ai_chat_histories tables.
|
|
9
|
+
*
|
|
10
|
+
* A2A task state (task metadata in Redis, workspace archives in GCS) is NOT
|
|
11
|
+
* here — that belongs to the A2A layer, see dsh-a2a's TaskStore backends.
|
|
12
|
+
*
|
|
13
|
+
* Resume caveat (docs/subsystems/persistence.md): constructor seeds do not
|
|
14
|
+
* emit events. On agent resume, only events >= `session.firstLiveSeq` fire
|
|
15
|
+
* on this stream — historical rows must come from the database itself, not
|
|
16
|
+
* from replaying the tap.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-storage
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
22
|
+
import { type DatabaseProvider } from './backends/database.js';
|
|
23
|
+
import { type BackfillSummary } from './backfill.js';
|
|
24
|
+
import { type SyncConfig } from './config.js';
|
|
25
|
+
import { type CreateTableResult } from './ddl.js';
|
|
26
|
+
import { type ConnectionTestResult, type HealthStatus } from './health.js';
|
|
27
|
+
import { type StorageToast } from './notify.js';
|
|
28
|
+
export declare const name = "dsh-storage";
|
|
29
|
+
export declare const inject: string[];
|
|
30
|
+
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
31
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
32
|
+
database: Schema<Schemastery.ObjectS<NoInfer<{
|
|
33
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
34
|
+
provider: Schema<"mysql" | "postgresql" | "sqlite" | "sqlserver", "mysql" | "postgresql" | "sqlite" | "sqlserver", "defined">;
|
|
35
|
+
url: Schema<string, string, "defined">;
|
|
36
|
+
timezone: Schema<string, string, "defined">;
|
|
37
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
38
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
39
|
+
provider: Schema<"mysql" | "postgresql" | "sqlite" | "sqlserver", "mysql" | "postgresql" | "sqlite" | "sqlserver", "defined">;
|
|
40
|
+
url: Schema<string, string, "defined">;
|
|
41
|
+
timezone: Schema<string, string, "defined">;
|
|
42
|
+
}>>, "plain">;
|
|
43
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
44
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
45
|
+
database: Schema<Schemastery.ObjectS<NoInfer<{
|
|
46
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
47
|
+
provider: Schema<"mysql" | "postgresql" | "sqlite" | "sqlserver", "mysql" | "postgresql" | "sqlite" | "sqlserver", "defined">;
|
|
48
|
+
url: Schema<string, string, "defined">;
|
|
49
|
+
timezone: Schema<string, string, "defined">;
|
|
50
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
51
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
52
|
+
provider: Schema<"mysql" | "postgresql" | "sqlite" | "sqlserver", "mysql" | "postgresql" | "sqlite" | "sqlserver", "defined">;
|
|
53
|
+
url: Schema<string, string, "defined">;
|
|
54
|
+
timezone: Schema<string, string, "defined">;
|
|
55
|
+
}>>, "plain">;
|
|
56
|
+
}>>, "plain">;
|
|
57
|
+
export interface StoragePluginConfig {
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
database: {
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
provider: DatabaseProvider;
|
|
62
|
+
url: string;
|
|
63
|
+
timezone?: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Explicit write API for a consumer's session-clear flow. */
|
|
67
|
+
export interface DshStorageService {
|
|
68
|
+
archiveSession(sessionId: string): Promise<string | null>;
|
|
69
|
+
}
|
|
70
|
+
/** Status/health RPC for the settings panel (Task 5) and notifications (Task 6). */
|
|
71
|
+
export interface DshStorageStatusService {
|
|
72
|
+
snapshot(): HealthStatus;
|
|
73
|
+
testConnection(cfg: SyncConfig): Promise<ConnectionTestResult>;
|
|
74
|
+
/** Panel 「建表」: provider-appropriate DDL via packaged prisma schemas. */
|
|
75
|
+
createTables(cfg: SyncConfig): Promise<CreateTableResult>;
|
|
76
|
+
/** Panel form load/persist — the file the backend watches (T3). */
|
|
77
|
+
getConfig(): {
|
|
78
|
+
config: SyncConfig;
|
|
79
|
+
error?: 'parse' | 'missing-field';
|
|
80
|
+
};
|
|
81
|
+
saveConfig(cfg: SyncConfig): {
|
|
82
|
+
config: SyncConfig;
|
|
83
|
+
};
|
|
84
|
+
/** Panel 「补传」 (T6): replay gap events from local session logs. */
|
|
85
|
+
backfill(): Promise<BackfillSummary>;
|
|
86
|
+
/** Toast notifications newer than the cursor (client polling, T6). */
|
|
87
|
+
toasts(payload: {
|
|
88
|
+
sinceId?: number;
|
|
89
|
+
}): StorageToast[];
|
|
90
|
+
}
|
|
91
|
+
export declare function apply(ctx: Context, config: StoragePluginConfig): void;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-accessible memory query tools (Phase 2): `memory_search` / `memory_read`.
|
|
3
|
+
*
|
|
4
|
+
* Registration contract (verified against the running DSH runtime):
|
|
5
|
+
* - `@deepseek-ai/dsh-tools` exposes a cordis `tools` service whose
|
|
6
|
+
* `register(definition)` inserts into the scoped tool layer; the definition
|
|
7
|
+
* is `{ name, description, parameters (plain JSON Schema), output: { schema,
|
|
8
|
+
* render(args, value) }, execute(args, exec), timeoutMs? }` (ToolsService
|
|
9
|
+
* inside the app bundle; working plugin idiom: ds-harness-remote
|
|
10
|
+
* `dist/remote-tools.js` → `ctx.inject(['tools'], (toolCtx) =>
|
|
11
|
+
* toolCtx.effect(() => tools.register(definition)))`).
|
|
12
|
+
* - Tool names are flat global strings (`remote_list_hosts` style), so these
|
|
13
|
+
* tools are `memory_search` / `memory_read`.
|
|
14
|
+
*
|
|
15
|
+
* Read-only SELECTs over the same connection config the mirror uses
|
|
16
|
+
* (createAdapter / configToUrl / loadConfig). Connections are disposed after
|
|
17
|
+
* every call. All user input is parameter-bound; LIKE wildcards in user
|
|
18
|
+
* keywords are escaped with an explicit `ESCAPE '~'` clause (works on all
|
|
19
|
+
* four providers — avoids backslash, which MySQL also treats as a string
|
|
20
|
+
* literal escape).
|
|
21
|
+
*
|
|
22
|
+
* Raw adapter binding detail (verified in @prisma/adapter-{mariadb,pg,mssql,libsql}
|
|
23
|
+
* 7.9.1 sources): each adapter maps `query.args[i]` through
|
|
24
|
+
* `mapArg(arg, query.argTypes[i])`, and the mssql adapter dereferences
|
|
25
|
+
* `argType.scalarType` unconditionally — binding any argument without
|
|
26
|
+
* argTypes throws (the ddl.ts comment describes this). The argType shape is
|
|
27
|
+
* the client runtime's `{ scalarType, arity }` record ('string' | 'int' |
|
|
28
|
+
* …, 'scalar' | 'list'), not the numeric ColumnTypeEnum. We therefore always
|
|
29
|
+
* pass argTypes and never bind Date objects (ISO strings compare correctly
|
|
30
|
+
* against DATETIME/TIMESTAMP/TEXT created_at on every provider).
|
|
31
|
+
*
|
|
32
|
+
* @module dsh-storage/memory-tools
|
|
33
|
+
*/
|
|
34
|
+
import { type DatabaseProvider } from './backends/database.js';
|
|
35
|
+
import { type SyncConfig } from './config.js';
|
|
36
|
+
/** Minimal cordis/tool surface we rely on — the package does not depend on dsh-tools types. */
|
|
37
|
+
export interface ToolDefinition {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
parameters: Record<string, unknown>;
|
|
41
|
+
output: {
|
|
42
|
+
schema: Record<string, unknown>;
|
|
43
|
+
render(args: unknown, value: unknown): Array<{
|
|
44
|
+
type: string;
|
|
45
|
+
text: string;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
execute(args: any, exec: {
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}): Promise<unknown>;
|
|
51
|
+
}
|
|
52
|
+
export interface ToolRegistrarContext {
|
|
53
|
+
inject(services: string[], callback: (ctx: ToolContext) => void): unknown;
|
|
54
|
+
}
|
|
55
|
+
export interface ToolContext {
|
|
56
|
+
get(service: string): unknown;
|
|
57
|
+
effect(factory: () => unknown): unknown;
|
|
58
|
+
}
|
|
59
|
+
/** One raw adapter query: `?` placeholders + positionally aligned args/argTypes. */
|
|
60
|
+
export interface RawQuery {
|
|
61
|
+
sql: string;
|
|
62
|
+
args: unknown[];
|
|
63
|
+
argTypes: Array<{
|
|
64
|
+
scalarType: string;
|
|
65
|
+
arity: 'scalar';
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Adapter connection from `createAdapter(...).connect()` (ddl.ts seam).
|
|
70
|
+
* All four adapters' `queryRaw` return `{ columnNames, rows: unknown[][] }`.
|
|
71
|
+
*/
|
|
72
|
+
export interface RawConnection {
|
|
73
|
+
queryRaw?(query: RawQuery): Promise<unknown>;
|
|
74
|
+
executeRaw(query: RawQuery): Promise<unknown>;
|
|
75
|
+
dispose?(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Escape LIKE wildcards in user input for use with `ESCAPE '~'`:
|
|
79
|
+
* `~` (the escape character itself), `%` and `_`. Everything else stays
|
|
80
|
+
* literal, so keywords match plain substrings.
|
|
81
|
+
*/
|
|
82
|
+
export declare function escapeLike(value: string): string;
|
|
83
|
+
/** Split a free-text query into non-empty keyword terms. */
|
|
84
|
+
export declare function splitKeywords(query: string): string[];
|
|
85
|
+
export interface SearchPlanOptions {
|
|
86
|
+
query: string;
|
|
87
|
+
device?: 'all' | 'current';
|
|
88
|
+
limit?: number;
|
|
89
|
+
since?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface SearchPlan {
|
|
92
|
+
sql: string;
|
|
93
|
+
args: unknown[];
|
|
94
|
+
argTypes: Array<{
|
|
95
|
+
scalarType: string;
|
|
96
|
+
arity: 'scalar';
|
|
97
|
+
}>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build the memory_search SELECT. Keywords OR across user+model rows
|
|
101
|
+
* (no type filter); `device:'current'` binds the configured device id;
|
|
102
|
+
* `since` (YYYY-MM-DD) filters `created_at >=` with an ISO 'T' delimiter so
|
|
103
|
+
* the string comparison also matches sqlite's TEXT timestamps.
|
|
104
|
+
*/
|
|
105
|
+
export declare function buildSearchSql(provider: DatabaseProvider, opts: SearchPlanOptions, deviceId?: string): SearchPlan;
|
|
106
|
+
export interface ReadPlanOptions {
|
|
107
|
+
sessionId: string;
|
|
108
|
+
around?: string;
|
|
109
|
+
limit?: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the memory_read SELECT. Both cases fetch the newest-first (DESC)
|
|
113
|
+
* `limit`-row window — with `around`, at-or-before the instant; without it,
|
|
114
|
+
* the most recent `limit` rows of the session. The executor always reverses
|
|
115
|
+
* the rows to chronological order before handing them to the model.
|
|
116
|
+
*/
|
|
117
|
+
export declare function buildReadSql(provider: DatabaseProvider, opts: ReadPlanOptions): SearchPlan;
|
|
118
|
+
/**
|
|
119
|
+
* Normalize adapter query results to row objects. The 7.9.1 adapters return
|
|
120
|
+
* `{ columnNames, rows: value[][] }`; tests and older shapes may already
|
|
121
|
+
* produce arrays of objects.
|
|
122
|
+
*/
|
|
123
|
+
export declare function normalizeRows(result: unknown): Array<Record<string, unknown>>;
|
|
124
|
+
/** First-index snippet ≤ max chars centered on the earliest keyword hit. */
|
|
125
|
+
export declare function snippetOf(content: string, keywords: string[], max?: number): string;
|
|
126
|
+
export interface SearchHit {
|
|
127
|
+
session_id: string;
|
|
128
|
+
device: string | null;
|
|
129
|
+
type: string;
|
|
130
|
+
created_at: string;
|
|
131
|
+
snippet: string;
|
|
132
|
+
}
|
|
133
|
+
export interface ReadMessage {
|
|
134
|
+
role: string;
|
|
135
|
+
type: string;
|
|
136
|
+
created_at: string;
|
|
137
|
+
content: string;
|
|
138
|
+
}
|
|
139
|
+
/** Shape the memory_search result list. */
|
|
140
|
+
export declare function toSearchHits(rows: Array<Record<string, unknown>>, keywords: string[]): SearchHit[];
|
|
141
|
+
/** Shape the memory_read conversation window. */
|
|
142
|
+
export declare function toReadMessages(rows: Array<Record<string, unknown>>, max?: number): ReadMessage[];
|
|
143
|
+
/** Friendly unconfigured-config error (shown to the model). */
|
|
144
|
+
export declare function notConfiguredError(reason?: string): Error;
|
|
145
|
+
/** Load the sync config and reject unless sync is enabled and complete. */
|
|
146
|
+
export declare function requireConfig(configPath?: string): SyncConfig;
|
|
147
|
+
/** Open a one-shot read-only adapter connection; caller disposes. */
|
|
148
|
+
export declare function connectReadOnly(config: SyncConfig): Promise<RawConnection>;
|
|
149
|
+
/** Run one SELECT over the connection and return row objects. */
|
|
150
|
+
export declare function runSelect(connection: RawConnection, query: RawQuery): Promise<Array<Record<string, unknown>>>;
|
|
151
|
+
export interface MemoryToolsDeps {
|
|
152
|
+
/** Config path override (tests); defaults to $DSH_HOME/dsh-storage.json. */
|
|
153
|
+
configPath?: string;
|
|
154
|
+
/** Connection factory override (tests): receives the resolved sync config. */
|
|
155
|
+
connect?: (config: SyncConfig) => Promise<RawConnection>;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build the two tool definitions. `deps.connect` defaults to a real
|
|
159
|
+
* read-only adapter connection created from the sync config and disposed
|
|
160
|
+
* after each execution.
|
|
161
|
+
*/
|
|
162
|
+
export declare function createMemoryTools(deps?: MemoryToolsDeps): ToolDefinition[];
|
|
163
|
+
/**
|
|
164
|
+
* Register the memory tools on the host `tools` service (dsh-tools):
|
|
165
|
+
* `ctx.inject(['tools'], (toolCtx) => toolCtx.effect(() => tools.register(def)))`
|
|
166
|
+
* — the ds-harness-remote idiom. Tools are host-side only; nothing client-side.
|
|
167
|
+
*/
|
|
168
|
+
export declare function registerMemoryTools(ctx: ToolRegistrarContext, deps?: MemoryToolsDeps): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 同步中断/恢复通知 (Task 6): the host side records toast notifications on
|
|
3
|
+
* HealthMonitor transitions; the browser client (src/client/toast.tsx) polls
|
|
4
|
+
* them through the status bridge and renders a React portal independent of
|
|
5
|
+
* the settings panel. The monitor already guarantees transitions only
|
|
6
|
+
* (same-state results never re-fire), so each record is a real state change.
|
|
7
|
+
*/
|
|
8
|
+
export interface StorageToast {
|
|
9
|
+
id: number;
|
|
10
|
+
kind: 'interrupted' | 'recovered';
|
|
11
|
+
text: string;
|
|
12
|
+
/** Secondary line (recovery backfill hint). */
|
|
13
|
+
hint?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
}
|
|
16
|
+
/** Minimal monitor surface wireNotifications needs (satisfied by HealthMonitor). */
|
|
17
|
+
export interface ToastCapableMonitor {
|
|
18
|
+
onTransition(cb: (to: string, from: string, status: {
|
|
19
|
+
lastError?: string;
|
|
20
|
+
}) => void): () => void;
|
|
21
|
+
}
|
|
22
|
+
/** 中断 toast: good → bad. */
|
|
23
|
+
export declare function notifyGoodToBad(lastError?: string): StorageToast;
|
|
24
|
+
/** 恢复 toast: bad → good, with a backfill hint for the gap. */
|
|
25
|
+
export declare function notifyBadToGood(): StorageToast;
|
|
26
|
+
/** Records newer than `sinceId` (polling cursor for the client). */
|
|
27
|
+
export declare function drainToasts(sinceId?: number): StorageToast[];
|
|
28
|
+
/** Test/reset hook: drop all history. */
|
|
29
|
+
export declare function clearToasts(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Wire the toast records onto a monitor's transitions. Good→bad strictly
|
|
32
|
+
* means ok → down (a config error was never "good"); bad→good covers
|
|
33
|
+
* down → ok and config-error → ok.
|
|
34
|
+
*/
|
|
35
|
+
export declare function wireNotifications(monitor: ToastCapableMonitor): () => void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects dsh session events onto MessageRow / SessionRow.
|
|
3
|
+
*
|
|
4
|
+
* Event shapes verified against the dsh persistence catalog
|
|
5
|
+
* (https://deepseek-harness.github.io/deepseek-harness/reference/persistence-catalog)
|
|
6
|
+
* and the @deepseek-ai/dsh-llm message types:
|
|
7
|
+
* - envelope: `{ type, seq, time, data }` — payload lives under `data`.
|
|
8
|
+
* - `user/message` and `assistant/message` produce rows; `tool/result` updates
|
|
9
|
+
* the matching assistant row's toolCalls instead of producing a tool row.
|
|
10
|
+
* - token usage (V3 log): `assistant/message.data.usage` is the settled
|
|
11
|
+
* step's accounting; a failed/cancelled attempt commits `assistant/attempt`
|
|
12
|
+
* instead, with the adapter's usage chunk preserved as a raw record in its
|
|
13
|
+
* embedded compact stream (recovered in usageSampleOf). `turn/end` carries
|
|
14
|
+
* none — it only serves as a rollup checkpoint.
|
|
15
|
+
* - compaction summaries enter the surface as a `user/message` with
|
|
16
|
+
* `surfaceOp: replace`, so they flow through the user path unchanged.
|
|
17
|
+
*/
|
|
18
|
+
import type { MessageRow } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Map one session event to zero or one message row. Returns null for events
|
|
21
|
+
* that should not be persisted as standalone rows (log-only events: chunks,
|
|
22
|
+
* turn/step lifecycle, approvals, ...).
|
|
23
|
+
*/
|
|
24
|
+
export declare function projectEvent(_session: any, event: any, sessionId: string): MessageRow | null;
|
|
25
|
+
/** Identify the assistant tool call answered by either supported result shape. */
|
|
26
|
+
export declare function toolResultCallId(event: any): string | undefined;
|
|
27
|
+
/** Update the persisted assistant call identified by a tool result. */
|
|
28
|
+
export declare function mergeToolResult(event: any, rows: MessageRow[]): MessageRow | null;
|
|
29
|
+
export interface UsageSample {
|
|
30
|
+
input: number;
|
|
31
|
+
output: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract one model call's usage, keyed by its durable event sequence.
|
|
35
|
+
*
|
|
36
|
+
* V3 session log (dsh-session@0.1.6-alpha.2):
|
|
37
|
+
* - `assistant/message.data.usage` — the settled call's accounting;
|
|
38
|
+
* - `assistant/attempt` — a failed/retried/cancelled call commits no surface
|
|
39
|
+
* message; the adapter's usage chunk survives as a raw `chunk` record in the
|
|
40
|
+
* embedded compact `stream` (deltas are packed, but usage is a raw type).
|
|
41
|
+
*
|
|
42
|
+
* Calls on one `(turn, step)` are additive: a retry is another billed model
|
|
43
|
+
* call, not a progressive snapshot. Only redelivery of the same event sequence
|
|
44
|
+
* is a duplicate. Totals include the cache buckets: cacheRead/cacheWrite are
|
|
45
|
+
* billed in their own buckets (observed cacheReadTokens ≫ inputTokens in dsh
|
|
46
|
+
* session logs — they are not subsets of inputTokens).
|
|
47
|
+
*/
|
|
48
|
+
export declare function usageSampleOf(event: any): {
|
|
49
|
+
key: string;
|
|
50
|
+
sample: UsageSample;
|
|
51
|
+
} | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host→browser bridge for the settings panel: exposes the dshStorageStatus
|
|
3
|
+
* service over the DSH connection generic-RPC channel `/dsh-storage`.
|
|
4
|
+
*
|
|
5
|
+
* Pattern verified against the installed reference plugins (ds-harness-remote
|
|
6
|
+
* `registerControlRoute`, dsh-video-skin settings.section client): the browser
|
|
7
|
+
* client calls `ctx.connection.rpc.call('/dsh-storage', endpoint, payload)`,
|
|
8
|
+
* which POSTs `{type:'client-request', rpcId, method, payload}` to
|
|
9
|
+
* `/<channel>/<endpoint>` and expects `{type:'server-response', rpcId,
|
|
10
|
+
* result:{ok:true,value}|{ok:false,error:{code,message,details}}}`. When the
|
|
11
|
+
* webServer service is available we serve that path with a prefix route
|
|
12
|
+
* (guarded by `connection.requestRejection` for authentication); otherwise we
|
|
13
|
+
* fall back to `connection.rpc.handle` for in-process carriers. Both halves
|
|
14
|
+
* stay optional (`ctx.get`), so profiles without a web carrier simply skip
|
|
15
|
+
* the bridge.
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import type { SyncConfig } from './config.js';
|
|
19
|
+
import type { CreateTableResult } from './ddl.js';
|
|
20
|
+
import type { ConnectionTestResult, HealthStatus } from './health.js';
|
|
21
|
+
import type { BackfillSummary } from './backfill.js';
|
|
22
|
+
import type { StorageToast } from './notify.js';
|
|
23
|
+
/** The surface bridged to the browser panel. */
|
|
24
|
+
export interface StatusService {
|
|
25
|
+
snapshot(): HealthStatus;
|
|
26
|
+
testConnection(cfg: SyncConfig): Promise<ConnectionTestResult>;
|
|
27
|
+
createTables(cfg: SyncConfig): Promise<CreateTableResult>;
|
|
28
|
+
getConfig(): {
|
|
29
|
+
config: SyncConfig;
|
|
30
|
+
error?: 'parse' | 'missing-field';
|
|
31
|
+
};
|
|
32
|
+
saveConfig(cfg: SyncConfig): {
|
|
33
|
+
config: SyncConfig;
|
|
34
|
+
};
|
|
35
|
+
/** Panel 「补传」: replay missing events from local session logs. */
|
|
36
|
+
backfill(): Promise<BackfillSummary>;
|
|
37
|
+
/** Toast notifications newer than the cursor (client polling). */
|
|
38
|
+
toasts(payload: {
|
|
39
|
+
sinceId?: number;
|
|
40
|
+
}): StorageToast[];
|
|
41
|
+
}
|
|
42
|
+
export declare const STATUS_CHANNEL = "/dsh-storage";
|
|
43
|
+
/**
|
|
44
|
+
* Register the /dsh-storage channel. Wire order mirrors ds-harness-remote's
|
|
45
|
+
* control route: an authenticated webServer prefix route when both services
|
|
46
|
+
* allow it, otherwise connection.rpc.handle for logical carriers.
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerStatusRoute(ctx: Context, service: StatusService): (() => void) | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared row types and the backend contract.
|
|
3
|
+
*
|
|
4
|
+
* Row shapes mirror the `ai_messages` / `ai_chat_histories` Prisma models
|
|
5
|
+
* (see prisma/schema.{mysql,postgresql,sqlite,sqlserver}.prisma), matching
|
|
6
|
+
* the source project's tables.
|
|
7
|
+
*/
|
|
8
|
+
/** One row in `ai_messages`. */
|
|
9
|
+
export interface MessageRow {
|
|
10
|
+
id: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
historyId: string | null;
|
|
13
|
+
/** 'user' | 'model' | 'tool' | 'utility_compressor' | plugin-defined */
|
|
14
|
+
type: string;
|
|
15
|
+
content: string;
|
|
16
|
+
thoughts?: unknown;
|
|
17
|
+
model?: string | null;
|
|
18
|
+
tokens?: unknown;
|
|
19
|
+
toolCalls?: unknown;
|
|
20
|
+
agentId?: string | null;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
}
|
|
24
|
+
/** One row in `ai_chat_histories` — a per-session rollup. */
|
|
25
|
+
export interface SessionRow {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
title?: string | null;
|
|
28
|
+
messageCount: number;
|
|
29
|
+
totalTokens: number;
|
|
30
|
+
firstMessageAt?: Date | null;
|
|
31
|
+
lastMessageAt?: Date | null;
|
|
32
|
+
/** Plugin bookkeeping persisted in the row's JSON metadata column. */
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A storage backend receives already-projected rows. Backend errors reject
|
|
37
|
+
* into the plugin's per-session queue, whose catch/log boundary keeps
|
|
38
|
+
* mirroring failures out of the agent loop.
|
|
39
|
+
*/
|
|
40
|
+
export interface StorageBackend {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
init?(): Promise<void>;
|
|
43
|
+
/** Read the stored session row, for seeding the live rollup on resume. */
|
|
44
|
+
readSession?(sessionId: string): Promise<SessionRow | null>;
|
|
45
|
+
/** Max message created_at per session across active rows (backfill gap detection). */
|
|
46
|
+
latestPerSession?(): Promise<Map<string, Date>>;
|
|
47
|
+
/** Read one persisted logical message, including archived rows for redelivery. */
|
|
48
|
+
readMessages(sessionId: string, messageId: string): Promise<MessageRow[]>;
|
|
49
|
+
/** Find the newest active model message containing this exact tool call. */
|
|
50
|
+
readToolCallMessages(sessionId: string, callId: string): Promise<MessageRow[]>;
|
|
51
|
+
upsertMessage(row: MessageRow): Promise<void>;
|
|
52
|
+
upsertSession(row: SessionRow): Promise<void>;
|
|
53
|
+
/** Atomically persist one event's optional message and its session rollup. */
|
|
54
|
+
upsertEvent(message: MessageRow | null, session: SessionRow): Promise<void>;
|
|
55
|
+
/** Move active messages into a queryable history row; null when already empty. */
|
|
56
|
+
archiveSession(sessionId: string): Promise<string | null>;
|
|
57
|
+
close?(): Promise<void>;
|
|
58
|
+
}
|
package/lib/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,129 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wfm-cn/dsh-storage",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Session storage mirror for DeepSeek Harness: projects the session event stream into MySQL/PostgreSQL/SQLite/SQL Server (ai_messages / ai_chat_histories) without replacing dsh's local persistence.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./client": {
|
|
15
|
+
"default": "./lib/client.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib",
|
|
20
|
+
"prisma",
|
|
21
|
+
"cordis.patch.yml",
|
|
22
|
+
"preview.png",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/2938236819/dsh-storage.git"
|
|
32
|
+
},
|
|
33
|
+
"dsh": {
|
|
34
|
+
"bundle": {
|
|
35
|
+
"patch": "./cordis.patch.yml"
|
|
36
|
+
},
|
|
37
|
+
"client": {
|
|
38
|
+
"inject": [
|
|
39
|
+
"@deepseek-ai/dsh-client-connection",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
41
|
+
],
|
|
42
|
+
"platform": "web"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "node scripts/prisma-generate.mjs && tsc -p tsconfig.build.json && npm run build:client",
|
|
47
|
+
"build:client": "esbuild src/client/index.ts --bundle --format=iife --outfile=lib/client.js --target=es2020 --minify --legal-comments=none",
|
|
48
|
+
"generate": "node scripts/prisma-generate.mjs",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
50
|
+
"test": "vitest run"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
54
|
+
"@prisma/client": "7.9.1",
|
|
55
|
+
"@prisma/adapter-mariadb": "7.9.1"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
59
|
+
"@deepseek-ai/dsh-session": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-rc.1 || >=0.2.1-rc.1 || >=0.2.2-rc.1",
|
|
60
|
+
"@prisma/adapter-libsql": "^7.9.1",
|
|
61
|
+
"@prisma/adapter-mariadb": "^7.9.1",
|
|
62
|
+
"@prisma/adapter-mssql": "^7.9.1",
|
|
63
|
+
"@prisma/adapter-pg": "^7.9.1",
|
|
64
|
+
"@prisma/client": "^7.9.1",
|
|
65
|
+
"react": ">=18.2.0 <19",
|
|
66
|
+
"@deepseek-ai/dsh-client-connection": ">=0.2.0-rc.1 <0.3.0-0",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.0-rc.1 <0.3.0-0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@prisma/adapter-libsql": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@prisma/adapter-mariadb": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@prisma/adapter-mssql": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@prisma/adapter-pg": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@prisma/client": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"react": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@deepseek-ai/dsh-client-connection": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
92
|
+
"optional": true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
97
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
98
|
+
"@prisma/adapter-libsql": "^7.9.1",
|
|
99
|
+
"@prisma/adapter-mariadb": "^7.9.1",
|
|
100
|
+
"@prisma/adapter-mssql": "^7.9.1",
|
|
101
|
+
"@prisma/adapter-pg": "^7.9.1",
|
|
102
|
+
"@prisma/client": "^7.9.1",
|
|
103
|
+
"@testing-library/dom": "^10.4.2",
|
|
104
|
+
"@testing-library/react": "^16.3.3",
|
|
105
|
+
"@types/node": "^22.0.0",
|
|
106
|
+
"@types/react": "^18.3.31",
|
|
107
|
+
"@types/react-dom": "^18.3.7",
|
|
108
|
+
"esbuild": "^0.28.2",
|
|
109
|
+
"jsdom": "^31.0.0",
|
|
110
|
+
"prisma": "^7.9.1",
|
|
111
|
+
"react": "^18.3.1",
|
|
112
|
+
"react-dom": "^18.3.1",
|
|
113
|
+
"typescript": "^5.6.3",
|
|
114
|
+
"vitest": "^4.1.11"
|
|
115
|
+
},
|
|
116
|
+
"engines": {
|
|
117
|
+
"node": "^20.19 || ^22.12 || >=24.0"
|
|
118
|
+
},
|
|
119
|
+
"pnpm": {
|
|
120
|
+
"onlyBuiltDependencies": [
|
|
121
|
+
"@prisma/engines",
|
|
122
|
+
"esbuild",
|
|
123
|
+
"prisma"
|
|
124
|
+
],
|
|
125
|
+
"patchedDependencies": {
|
|
126
|
+
"@prisma/adapter-mariadb@7.9.1": "patches/@prisma__adapter-mariadb@7.9.1.patch"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
package/preview.png
ADDED
|
Binary file
|