@wfm-cn/dsh-storage 0.0.0-stage → 0.2.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -2
  4. package/cordis.patch.yml +3 -0
  5. package/lib/backends/database.js +578 -0
  6. package/lib/backfill.js +331 -0
  7. package/lib/client/index.js +79 -0
  8. package/lib/client/panel.js +299 -0
  9. package/lib/client/toast.js +66 -0
  10. package/lib/client.js +1 -0
  11. package/lib/compaction.js +108 -0
  12. package/lib/config.js +171 -0
  13. package/lib/ddl.js +323 -0
  14. package/lib/generated/mysql/browser.js +17 -0
  15. package/lib/generated/mysql/client.js +34 -0
  16. package/lib/generated/mysql/commonInputTypes.js +10 -0
  17. package/lib/generated/mysql/enums.js +10 -0
  18. package/lib/generated/mysql/internal/class.js +49 -0
  19. package/lib/generated/mysql/internal/prismaNamespace.js +146 -0
  20. package/lib/generated/mysql/internal/prismaNamespaceBrowser.js +117 -0
  21. package/lib/generated/mysql/models/AiChatHistory.js +1 -0
  22. package/lib/generated/mysql/models/AiMessage.js +1 -0
  23. package/lib/generated/mysql/models.js +1 -0
  24. package/lib/generated/postgresql/browser.js +17 -0
  25. package/lib/generated/postgresql/client.js +34 -0
  26. package/lib/generated/postgresql/commonInputTypes.js +10 -0
  27. package/lib/generated/postgresql/enums.js +10 -0
  28. package/lib/generated/postgresql/internal/class.js +49 -0
  29. package/lib/generated/postgresql/internal/prismaNamespace.js +131 -0
  30. package/lib/generated/postgresql/internal/prismaNamespaceBrowser.js +102 -0
  31. package/lib/generated/postgresql/models/AiChatHistory.js +1 -0
  32. package/lib/generated/postgresql/models/AiMessage.js +1 -0
  33. package/lib/generated/postgresql/models.js +1 -0
  34. package/lib/generated/sqlite/browser.js +17 -0
  35. package/lib/generated/sqlite/client.js +34 -0
  36. package/lib/generated/sqlite/commonInputTypes.js +10 -0
  37. package/lib/generated/sqlite/enums.js +10 -0
  38. package/lib/generated/sqlite/internal/class.js +49 -0
  39. package/lib/generated/sqlite/internal/prismaNamespace.js +128 -0
  40. package/lib/generated/sqlite/internal/prismaNamespaceBrowser.js +99 -0
  41. package/lib/generated/sqlite/models/AiChatHistory.js +1 -0
  42. package/lib/generated/sqlite/models/AiMessage.js +1 -0
  43. package/lib/generated/sqlite/models.js +1 -0
  44. package/lib/generated/sqlserver/browser.js +17 -0
  45. package/lib/generated/sqlserver/client.js +34 -0
  46. package/lib/generated/sqlserver/commonInputTypes.js +10 -0
  47. package/lib/generated/sqlserver/enums.js +10 -0
  48. package/lib/generated/sqlserver/internal/class.js +49 -0
  49. package/lib/generated/sqlserver/internal/prismaNamespace.js +119 -0
  50. package/lib/generated/sqlserver/internal/prismaNamespaceBrowser.js +90 -0
  51. package/lib/generated/sqlserver/models/AiChatHistory.js +1 -0
  52. package/lib/generated/sqlserver/models/AiMessage.js +1 -0
  53. package/lib/generated/sqlserver/models.js +1 -0
  54. package/lib/health.js +180 -0
  55. package/lib/index.js +676 -0
  56. package/lib/notify.js +56 -0
  57. package/lib/projector.js +191 -0
  58. package/lib/status-route.js +172 -0
  59. package/lib/types/backends/database.d.ts +78 -0
  60. package/lib/types/backfill.d.ts +67 -0
  61. package/lib/types/client/index.d.ts +12 -0
  62. package/lib/types/client/panel.d.ts +79 -0
  63. package/lib/types/client/toast.d.ts +38 -0
  64. package/lib/types/compaction.d.ts +3 -0
  65. package/lib/types/config.d.ts +52 -0
  66. package/lib/types/ddl.d.ts +49 -0
  67. package/lib/types/generated/mysql/browser.d.ts +14 -0
  68. package/lib/types/generated/mysql/client.d.ts +33 -0
  69. package/lib/types/generated/mysql/commonInputTypes.d.ts +396 -0
  70. package/lib/types/generated/mysql/enums.d.ts +1 -0
  71. package/lib/types/generated/mysql/internal/class.d.ts +143 -0
  72. package/lib/types/generated/mysql/internal/prismaNamespace.d.ts +709 -0
  73. package/lib/types/generated/mysql/internal/prismaNamespaceBrowser.d.ts +112 -0
  74. package/lib/types/generated/mysql/models/AiChatHistory.d.ts +1258 -0
  75. package/lib/types/generated/mysql/models/AiMessage.d.ts +1359 -0
  76. package/lib/types/generated/mysql/models.d.ts +3 -0
  77. package/lib/types/generated/postgresql/browser.d.ts +14 -0
  78. package/lib/types/generated/postgresql/client.d.ts +33 -0
  79. package/lib/types/generated/postgresql/commonInputTypes.d.ts +392 -0
  80. package/lib/types/generated/postgresql/enums.d.ts +1 -0
  81. package/lib/types/generated/postgresql/internal/class.d.ts +143 -0
  82. package/lib/types/generated/postgresql/internal/prismaNamespace.d.ts +728 -0
  83. package/lib/types/generated/postgresql/internal/prismaNamespaceBrowser.d.ts +95 -0
  84. package/lib/types/generated/postgresql/models/AiChatHistory.d.ts +1373 -0
  85. package/lib/types/generated/postgresql/models/AiMessage.d.ts +1496 -0
  86. package/lib/types/generated/postgresql/models.d.ts +3 -0
  87. package/lib/types/generated/sqlite/browser.d.ts +14 -0
  88. package/lib/types/generated/sqlite/client.d.ts +33 -0
  89. package/lib/types/generated/sqlite/commonInputTypes.d.ts +373 -0
  90. package/lib/types/generated/sqlite/enums.d.ts +1 -0
  91. package/lib/types/generated/sqlite/internal/class.d.ts +143 -0
  92. package/lib/types/generated/sqlite/internal/prismaNamespace.d.ts +705 -0
  93. package/lib/types/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +92 -0
  94. package/lib/types/generated/sqlite/models/AiChatHistory.d.ts +1371 -0
  95. package/lib/types/generated/sqlite/models/AiMessage.d.ts +1493 -0
  96. package/lib/types/generated/sqlite/models.d.ts +3 -0
  97. package/lib/types/generated/sqlserver/browser.d.ts +14 -0
  98. package/lib/types/generated/sqlserver/client.d.ts +33 -0
  99. package/lib/types/generated/sqlserver/commonInputTypes.d.ts +333 -0
  100. package/lib/types/generated/sqlserver/enums.d.ts +1 -0
  101. package/lib/types/generated/sqlserver/internal/class.d.ts +143 -0
  102. package/lib/types/generated/sqlserver/internal/prismaNamespace.d.ts +669 -0
  103. package/lib/types/generated/sqlserver/internal/prismaNamespaceBrowser.d.ts +80 -0
  104. package/lib/types/generated/sqlserver/models/AiChatHistory.d.ts +1257 -0
  105. package/lib/types/generated/sqlserver/models/AiMessage.d.ts +1375 -0
  106. package/lib/types/generated/sqlserver/models.d.ts +3 -0
  107. package/lib/types/health.d.ts +52 -0
  108. package/lib/types/index.d.ts +91 -0
  109. package/lib/types/notify.d.ts +35 -0
  110. package/lib/types/projector.d.ts +51 -0
  111. package/lib/types/status-route.d.ts +48 -0
  112. package/lib/types/types.d.ts +58 -0
  113. package/lib/types.js +8 -0
  114. package/package.json +126 -3
  115. package/preview.png +0 -0
  116. package/prisma/schema.mysql.prisma +78 -0
  117. package/prisma/schema.postgresql.prisma +70 -0
  118. package/prisma/schema.sqlite.prisma +70 -0
  119. package/prisma/schema.sqlserver.prisma +76 -0
@@ -0,0 +1,3 @@
1
+ export type * from './models/AiMessage.js';
2
+ export type * from './models/AiChatHistory.js';
3
+ export type * from './commonInputTypes.js';
@@ -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,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
@@ -0,0 +1,8 @@
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
+ export {};
package/package.json CHANGED
@@ -1,6 +1,129 @@
1
1
  {
2
2
  "name": "@wfm-cn/dsh-storage",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.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
+ }
6
129
  }
package/preview.png ADDED
Binary file
@@ -0,0 +1,78 @@
1
+ // Prisma schema for the dsh-storage database backend — MySQL variant (Prisma 7).
2
+ //
3
+ // Table layout matches the source project's ai_messages / ai_chat_histories
4
+ // (packages/core/prisma/schema.prisma) so existing data stays compatible:
5
+ // - `type` values come from the dsh session event map
6
+ // ('user' | 'model' | 'tool' | ...) instead of gemini-cli message roles.
7
+ // - `agent_id` distinguishes the main agent from subagent writers.
8
+ // - no user_id: tenancy is carried by session_id, not a column.
9
+ //
10
+ // The client is pre-generated at package build time (`pnpm generate`, output
11
+ // src/generated/mysql, compiled into lib/generated) — consumers do NOT run
12
+ // `prisma generate`. To create/upgrade tables, run `prisma db push` /
13
+ // migrations against this file; the connection URL lives in prisma.config.ts
14
+ // (Prisma 7 dropped datasource.url from the schema).
15
+
16
+ generator client {
17
+ provider = "prisma-client"
18
+ output = "../src/generated/mysql"
19
+ importFileExtension = "js"
20
+ }
21
+
22
+ datasource db {
23
+ provider = "mysql"
24
+ }
25
+
26
+ /// User and assistant messages; tool results update the assistant row's tool_calls.
27
+ model AiMessage {
28
+ id String @id @default(cuid()) @db.Char(36)
29
+ sessionId String @map("session_id") @db.VarChar(100)
30
+ historyId String? @map("history_id") @db.Char(36)
31
+ type String @db.VarChar(50)
32
+ content String @db.LongText
33
+ thoughts Json?
34
+ model String? @db.VarChar(100)
35
+ tokens Json?
36
+ toolCalls Json? @map("tool_calls")
37
+ agentId String? @map("agent_id") @db.VarChar(100)
38
+ metadata Json?
39
+ device String? @map("device") @db.VarChar(100)
40
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0)
41
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamp(0)
42
+ deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
43
+
44
+ // Relations
45
+ history AiChatHistory? @relation(fields: [historyId], references: [id], onDelete: NoAction)
46
+
47
+ @@index([sessionId], map: "idx_session")
48
+ @@index([historyId], map: "idx_history")
49
+ @@index([createdAt], map: "idx_created")
50
+ @@index([deletedAt], map: "idx_deleted")
51
+ @@index([agentId], map: "idx_agent")
52
+ @@index([sessionId, agentId, type, createdAt], map: "idx_session_agent_type_created")
53
+ @@index([sessionId, historyId, deletedAt, type, createdAt, id], map: "idx_tool_call_lookup")
54
+ @@index([sessionId, deletedAt, updatedAt, id], map: "idx_archive_recovery")
55
+ @@map("ai_messages")
56
+ }
57
+
58
+ /// Per-session rollup, maintained incrementally by the projector.
59
+ model AiChatHistory {
60
+ id String @id @default(cuid()) @db.Char(36)
61
+ sessionId String @map("session_id") @db.VarChar(100)
62
+ title String? @db.VarChar(255)
63
+ summary String? @db.Text
64
+ messageCount Int @default(0) @map("message_count")
65
+ totalTokens BigInt @default(0) @map("total_tokens")
66
+ firstMessageAt DateTime? @map("first_message_at") @db.DateTime(6)
67
+ lastMessageAt DateTime? @map("last_message_at") @db.DateTime(6)
68
+ metadata Json?
69
+ device String? @map("device") @db.VarChar(100)
70
+ deletedAt DateTime? @map("deleted_at") @db.DateTime(6)
71
+
72
+ // Relations
73
+ messages AiMessage[]
74
+
75
+ @@index([sessionId], map: "idx_chat_history_session")
76
+ @@index([deletedAt], map: "idx_chat_history_deleted")
77
+ @@map("ai_chat_histories")
78
+ }
@@ -0,0 +1,70 @@
1
+ // Prisma schema for the dsh-storage database backend — PostgreSQL variant (Prisma 7).
2
+ //
3
+ // Same models as schema.mysql.prisma, with PostgreSQL native types
4
+ // (`@db.Text` instead of `@db.LongText`, `@db.Timestamp` instead of
5
+ // `@db.DateTime`). Client pre-generation and table creation work as described
6
+ // in schema.mysql.prisma's header.
7
+
8
+ generator client {
9
+ provider = "prisma-client"
10
+ output = "../src/generated/postgresql"
11
+ importFileExtension = "js"
12
+ }
13
+
14
+ datasource db {
15
+ provider = "postgresql"
16
+ }
17
+
18
+ /// User and assistant messages; tool results update the assistant row's tool_calls.
19
+ model AiMessage {
20
+ id String @id @default(cuid()) @db.Char(36)
21
+ sessionId String @map("session_id") @db.VarChar(100)
22
+ historyId String? @map("history_id") @db.Char(36)
23
+ type String @db.VarChar(50)
24
+ content String @db.Text
25
+ thoughts Json?
26
+ model String? @db.VarChar(100)
27
+ tokens Json?
28
+ toolCalls Json? @map("tool_calls")
29
+ agentId String? @map("agent_id") @db.VarChar(100)
30
+ metadata Json?
31
+ device String? @map("device") @db.VarChar(100)
32
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0)
33
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamp(0)
34
+ deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
35
+
36
+ // Relations
37
+ history AiChatHistory? @relation(fields: [historyId], references: [id], onDelete: NoAction)
38
+
39
+ @@index([sessionId], map: "idx_session")
40
+ @@index([historyId], map: "idx_history")
41
+ @@index([createdAt], map: "idx_created")
42
+ @@index([deletedAt], map: "idx_deleted")
43
+ @@index([agentId], map: "idx_agent")
44
+ @@index([sessionId, agentId, type, createdAt], map: "idx_session_agent_type_created")
45
+ @@index([sessionId, historyId, deletedAt, type, createdAt, id], map: "idx_tool_call_lookup")
46
+ @@index([sessionId, deletedAt, updatedAt, id], map: "idx_archive_recovery")
47
+ @@map("ai_messages")
48
+ }
49
+
50
+ /// Per-session rollup, maintained incrementally by the projector.
51
+ model AiChatHistory {
52
+ id String @id @default(cuid()) @db.Char(36)
53
+ sessionId String @map("session_id") @db.VarChar(100)
54
+ title String? @db.VarChar(255)
55
+ summary String? @db.Text
56
+ messageCount Int @default(0) @map("message_count")
57
+ totalTokens BigInt @default(0) @map("total_tokens")
58
+ firstMessageAt DateTime? @map("first_message_at") @db.Timestamp(6)
59
+ lastMessageAt DateTime? @map("last_message_at") @db.Timestamp(6)
60
+ metadata Json?
61
+ device String? @map("device") @db.VarChar(100)
62
+ deletedAt DateTime? @map("deleted_at") @db.Timestamp(6)
63
+
64
+ // Relations
65
+ messages AiMessage[]
66
+
67
+ @@index([sessionId], map: "idx_chat_history_session")
68
+ @@index([deletedAt], map: "idx_chat_history_deleted")
69
+ @@map("ai_chat_histories")
70
+ }