dsh-plugin-sessiongraph-service 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CC ZHAO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A three-line re-statement of `assertNever`, owned here instead of imported from the host.
3
+ *
4
+ * `@deepseek-ai/dsh-llm` exported this through 0.1.x and dropped it in 0.2.x (the implementation
5
+ * moved to `@deepseek-ai/dsh-util-values`, which 0.1.x does not ship). A static import of a symbol
6
+ * the host no longer exports parses fine and fails at load, which removes the whole consumer plugin
7
+ * from the host instead of failing at one unreachable branch.
8
+ * @module dsh-plugin-sessiongraph-service/assert
9
+ */
10
+ /**
11
+ * Compile-time exhaustiveness check for a discriminated union.
12
+ * @param value - the value that must never be reached.
13
+ * @returns never; throws when the value is actually reachable.
14
+ */
15
+ export declare function assertNever(value: never): never;
16
+ //# sourceMappingURL=assert.d.ts.map
package/lib/assert.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A three-line re-statement of `assertNever`, owned here instead of imported from the host.
3
+ *
4
+ * `@deepseek-ai/dsh-llm` exported this through 0.1.x and dropped it in 0.2.x (the implementation
5
+ * moved to `@deepseek-ai/dsh-util-values`, which 0.1.x does not ship). A static import of a symbol
6
+ * the host no longer exports parses fine and fails at load, which removes the whole consumer plugin
7
+ * from the host instead of failing at one unreachable branch.
8
+ * @module dsh-plugin-sessiongraph-service/assert
9
+ */
10
+ /**
11
+ * Compile-time exhaustiveness check for a discriminated union.
12
+ * @param value - the value that must never be reached.
13
+ * @returns never; throws when the value is actually reachable.
14
+ */
15
+ export function assertNever(value) {
16
+ throw new Error(`unreachable: ${JSON.stringify(value)}`);
17
+ }
18
+ //# sourceMappingURL=assert.js.map
package/lib/brand.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * dsh-plugin-sessiongraph's owned branded ids: {@link SessiongraphStoreId}, the identity a read
3
+ * provider reserves on `ctx.sessiongraph`; {@link SessiongraphIndexerId}, the identity a write
4
+ * provider reserves; and {@link SessiongraphSessionId}, a harness session id taken verbatim from a
5
+ * session log's header.
6
+ *
7
+ * The brand is declared here rather than borrowed from `@deepseek-ai/dsh-brand` on purpose: a host
8
+ * utility that exists in one DSH line and not another turns into a load-time failure that removes
9
+ * the whole plugin, which is exactly the failure mode a four-line `unique symbol` avoids. Keeping
10
+ * each type with its factory lets `index.ts` re-export all three under one name.
11
+ * @module dsh-plugin-sessiongraph-service/brand
12
+ */
13
+ declare const SESSIONGRAPH_STORE_BRAND: unique symbol;
14
+ declare const SESSIONGRAPH_INDEXER_BRAND: unique symbol;
15
+ declare const SESSIONGRAPH_SESSION_BRAND: unique symbol;
16
+ /** Opaque store-provider identity, reserved at registration and released by the disposer. */
17
+ export type SessiongraphStoreId = string & {
18
+ readonly [SESSIONGRAPH_STORE_BRAND]: true;
19
+ };
20
+ /**
21
+ * Brand a string as a {@link SessiongraphStoreId}. No validation — the registry rejects an empty id
22
+ * at registration.
23
+ * @param id - the provider's stable identifier.
24
+ * @returns the same string, branded.
25
+ */
26
+ export declare function SessiongraphStoreId(id: string): SessiongraphStoreId;
27
+ /** Opaque indexer-provider identity, reserved at registration and released by the disposer. */
28
+ export type SessiongraphIndexerId = string & {
29
+ readonly [SESSIONGRAPH_INDEXER_BRAND]: true;
30
+ };
31
+ /**
32
+ * Brand a string as a {@link SessiongraphIndexerId}. No validation — the registry rejects an empty id
33
+ * at registration.
34
+ * @param id - the provider's stable identifier.
35
+ * @returns the same string, branded.
36
+ */
37
+ export declare function SessiongraphIndexerId(id: string): SessiongraphIndexerId;
38
+ /** Identity of one harness session, as recorded in its log header. */
39
+ export type SessiongraphSessionId = string & {
40
+ readonly [SESSIONGRAPH_SESSION_BRAND]: true;
41
+ };
42
+ /**
43
+ * Brand a string as a {@link SessiongraphSessionId}.
44
+ * @param id - the session id read from a session log header.
45
+ * @returns the same string, branded.
46
+ */
47
+ export declare function SessiongraphSessionId(id: string): SessiongraphSessionId;
48
+ export {};
49
+ //# sourceMappingURL=brand.d.ts.map
package/lib/brand.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * dsh-plugin-sessiongraph's owned branded ids: {@link SessiongraphStoreId}, the identity a read
3
+ * provider reserves on `ctx.sessiongraph`; {@link SessiongraphIndexerId}, the identity a write
4
+ * provider reserves; and {@link SessiongraphSessionId}, a harness session id taken verbatim from a
5
+ * session log's header.
6
+ *
7
+ * The brand is declared here rather than borrowed from `@deepseek-ai/dsh-brand` on purpose: a host
8
+ * utility that exists in one DSH line and not another turns into a load-time failure that removes
9
+ * the whole plugin, which is exactly the failure mode a four-line `unique symbol` avoids. Keeping
10
+ * each type with its factory lets `index.ts` re-export all three under one name.
11
+ * @module dsh-plugin-sessiongraph-service/brand
12
+ */
13
+ /**
14
+ * Brand a string as a {@link SessiongraphStoreId}. No validation — the registry rejects an empty id
15
+ * at registration.
16
+ * @param id - the provider's stable identifier.
17
+ * @returns the same string, branded.
18
+ */
19
+ export function SessiongraphStoreId(id) {
20
+ return id;
21
+ }
22
+ /**
23
+ * Brand a string as a {@link SessiongraphIndexerId}. No validation — the registry rejects an empty id
24
+ * at registration.
25
+ * @param id - the provider's stable identifier.
26
+ * @returns the same string, branded.
27
+ */
28
+ export function SessiongraphIndexerId(id) {
29
+ return id;
30
+ }
31
+ /**
32
+ * Brand a string as a {@link SessiongraphSessionId}.
33
+ * @param id - the session id read from a session log header.
34
+ * @returns the same string, branded.
35
+ */
36
+ export function SessiongraphSessionId(id) {
37
+ return id;
38
+ }
39
+ //# sourceMappingURL=brand.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Service Definition for the session-graph capability seam (`ctx.sessiongraph`): a store-provider
3
+ * and indexer-provider registry over local conversation history, plus the normalized query
4
+ * vocabulary — `search`, `session`, `timeline`, `workspaces`, and `status`.
5
+ *
6
+ * A store reserves a branded id at registration and declares which index roots it can serve through
7
+ * {@link SessiongraphStoreProvider.indexes}. Selection asks every registered store per query and
8
+ * requires exactly one claimant, so registration and hot-reload order never change routing; zero
9
+ * claimants and several claimants are both loud failures rather than a silent pick. The seam carries
10
+ * no session text and performs no filesystem access of its own.
11
+ *
12
+ * Unlike the code-graph seam, `status` is answerable without an index: a caller that has never
13
+ * indexed anything needs to learn that fact, and every other operation throws
14
+ * `SESSIONGRAPH_UNAVAILABLE` instead of pretending the store is empty.
15
+ * @module dsh-plugin-sessiongraph-service
16
+ */
17
+ import { Context, Service } from '@deepseek-ai/cordis';
18
+ import type { SessiongraphIndexer, SessiongraphIndexOptions, SessiongraphIndexReport, SessiongraphRequest, SessiongraphResultFor, SessiongraphService, SessiongraphStoreProvider } from './types.ts';
19
+ export { assertNever } from './assert.ts';
20
+ export { SessiongraphIndexerId, SessiongraphSessionId, SessiongraphStoreId } from './brand.ts';
21
+ export { ENTRY_FTS_TABLE, ENTRY_TABLE, INDEX_RELATIVE_PATH, META_DISCOVERED, META_FAILED, META_INDEXED_AT, META_SCHEMA_VERSION, SCHEMA_STATEMENTS, SCHEMA_VERSION, SESSION_TABLE, } from './schema.ts';
22
+ export { highlightTerms, isCjk, matchExpression, tokenize } from './tokenize.ts';
23
+ export { SessiongraphError } from './types.ts';
24
+ export type { SessiongraphEntry, SessiongraphEntryKind, SessiongraphErrorCode, SessiongraphHit, SessiongraphIndexer, SessiongraphIndexOptions, SessiongraphIndexReport, SessiongraphOperation, SessiongraphRequest, SessiongraphRequestBase, SessiongraphResult, SessiongraphResultFor, SessiongraphSearchRequest, SessiongraphSearchResult, SessiongraphService, SessiongraphSessionRequest, SessiongraphSessionResult, SessiongraphSessionSummary, SessiongraphStatusRequest, SessiongraphStatusResult, SessiongraphStoreProvider, SessiongraphTimelineRequest, SessiongraphTimelineResult, SessiongraphWorkspaceStat, SessiongraphWorkspacesRequest, SessiongraphWorkspacesResult, } from './types.ts';
25
+ export { SESSIONGRAPH_ENTRY_KINDS, SESSIONGRAPH_OPERATIONS } from './types.ts';
26
+ /**
27
+ * `ctx.sessiongraph`. Holds the store and indexer reservations; selection reads them per call so a
28
+ * provider that unloads mid-session stops serving without leaving a stale route behind.
29
+ */
30
+ export declare class Sessiongraph extends Service implements SessiongraphService {
31
+ private readonly stores;
32
+ private readonly indexers;
33
+ constructor(ctx: Context);
34
+ registerStore(provider: SessiongraphStoreProvider): () => void;
35
+ registerIndexer(provider: SessiongraphIndexer): () => void;
36
+ available(indexRoot: string, signal?: AbortSignal): Promise<boolean>;
37
+ query<R extends SessiongraphRequest>(request: R, signal?: AbortSignal): Promise<SessiongraphResultFor<R>>;
38
+ index(indexRoot: string, options?: SessiongraphIndexOptions, signal?: AbortSignal): Promise<SessiongraphIndexReport>;
39
+ /**
40
+ * The one store that serves `indexRoot`. Every registered store is asked concurrently, so the
41
+ * answer does not depend on registration order; zero and several claimants both throw.
42
+ * @param indexRoot - directory the request targets.
43
+ * @param signal - aborts the availability checks.
44
+ * @returns the single claiming store.
45
+ */
46
+ private select;
47
+ /**
48
+ * The one indexer that can build `indexRoot`. Same one-claimant rule as {@link Sessiongraph.select}.
49
+ * @param indexRoot - directory to build into.
50
+ * @param signal - aborts the availability checks.
51
+ * @returns the single claiming indexer.
52
+ */
53
+ private selectIndexer;
54
+ }
55
+ export default Sessiongraph;
56
+ /** Cordis plugin name for loader diagnostics. */
57
+ export declare const name = "sessiongraph";
58
+ /**
59
+ * Publish the seam. Registers nothing else: providers arrive from their own plugins, which is what
60
+ * lets a deployment swap the store or the indexer without touching the model-facing tool.
61
+ * @param ctx - Cordis context to install into.
62
+ */
63
+ export declare function apply(ctx: Context): void;
64
+ declare module '@deepseek-ai/cordis' {
65
+ interface Context {
66
+ /** The session-graph seam. Present whenever this package's plugin is loaded. */
67
+ sessiongraph: SessiongraphService;
68
+ }
69
+ }
70
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Service Definition for the session-graph capability seam (`ctx.sessiongraph`): a store-provider
3
+ * and indexer-provider registry over local conversation history, plus the normalized query
4
+ * vocabulary — `search`, `session`, `timeline`, `workspaces`, and `status`.
5
+ *
6
+ * A store reserves a branded id at registration and declares which index roots it can serve through
7
+ * {@link SessiongraphStoreProvider.indexes}. Selection asks every registered store per query and
8
+ * requires exactly one claimant, so registration and hot-reload order never change routing; zero
9
+ * claimants and several claimants are both loud failures rather than a silent pick. The seam carries
10
+ * no session text and performs no filesystem access of its own.
11
+ *
12
+ * Unlike the code-graph seam, `status` is answerable without an index: a caller that has never
13
+ * indexed anything needs to learn that fact, and every other operation throws
14
+ * `SESSIONGRAPH_UNAVAILABLE` instead of pretending the store is empty.
15
+ * @module dsh-plugin-sessiongraph-service
16
+ */
17
+ import { join } from 'node:path';
18
+ import { Service } from '@deepseek-ai/cordis';
19
+ import { INDEX_RELATIVE_PATH } from "./schema.js";
20
+ import { SessiongraphError } from "./types.js";
21
+ export { assertNever } from "./assert.js";
22
+ export { SessiongraphIndexerId, SessiongraphSessionId, SessiongraphStoreId } from "./brand.js";
23
+ export { ENTRY_FTS_TABLE, ENTRY_TABLE, INDEX_RELATIVE_PATH, META_DISCOVERED, META_FAILED, META_INDEXED_AT, META_SCHEMA_VERSION, SCHEMA_STATEMENTS, SCHEMA_VERSION, SESSION_TABLE, } from "./schema.js";
24
+ export { highlightTerms, isCjk, matchExpression, tokenize } from "./tokenize.js";
25
+ export { SessiongraphError } from "./types.js";
26
+ export { SESSIONGRAPH_ENTRY_KINDS, SESSIONGRAPH_OPERATIONS } from "./types.js";
27
+ /**
28
+ * `ctx.sessiongraph`. Holds the store and indexer reservations; selection reads them per call so a
29
+ * provider that unloads mid-session stops serving without leaving a stale route behind.
30
+ */
31
+ export class Sessiongraph extends Service {
32
+ stores = new Map();
33
+ indexers = new Map();
34
+ constructor(ctx) {
35
+ super(ctx, 'sessiongraph');
36
+ }
37
+ registerStore(provider) {
38
+ const id = provider.id;
39
+ if (id.trim() === '') {
40
+ throw new SessiongraphError('a session-graph store id must be a non-empty string', 'SESSIONGRAPH_INVALID_PROVIDER');
41
+ }
42
+ if (this.stores.has(id)) {
43
+ throw new SessiongraphError(`a session-graph store with id "${id}" is already registered`, 'SESSIONGRAPH_CONFLICT');
44
+ }
45
+ const dispose = this.ctx.effect(function* () {
46
+ this.stores.set(id, provider);
47
+ yield () => {
48
+ this.stores.delete(id);
49
+ };
50
+ }.bind(this), 'sessiongraph.registerStore()');
51
+ return () => void dispose();
52
+ }
53
+ registerIndexer(provider) {
54
+ const id = provider.id;
55
+ if (id.trim() === '') {
56
+ throw new SessiongraphError('a session-graph indexer id must be a non-empty string', 'SESSIONGRAPH_INVALID_PROVIDER');
57
+ }
58
+ if (this.indexers.has(id)) {
59
+ throw new SessiongraphError(`a session-graph indexer with id "${id}" is already registered`, 'SESSIONGRAPH_CONFLICT');
60
+ }
61
+ const dispose = this.ctx.effect(function* () {
62
+ this.indexers.set(id, provider);
63
+ yield () => {
64
+ this.indexers.delete(id);
65
+ };
66
+ }.bind(this), 'sessiongraph.registerIndexer()');
67
+ return () => void dispose();
68
+ }
69
+ async available(indexRoot, signal) {
70
+ if (this.stores.size === 0)
71
+ return false;
72
+ const candidates = [...this.stores.values()];
73
+ const claims = await Promise.all(candidates.map(store => store.indexes(indexRoot, signal)));
74
+ return claims.some(Boolean);
75
+ }
76
+ async query(request, signal) {
77
+ // `status` is the one operation answerable with nothing indexed: a caller whose root has no
78
+ // store claim needs to learn *that fact*, not receive the same unavailable error every other
79
+ // operation raises. A conflict is still a real error — two stores claiming one root is a
80
+ // deployment mistake, and silently reporting "not indexed" would hide it.
81
+ let store;
82
+ try {
83
+ store = await this.select(request.indexRoot, signal);
84
+ }
85
+ catch (error) {
86
+ if (request.operation === 'status' && error instanceof SessiongraphError && error.code === 'SESSIONGRAPH_UNAVAILABLE') {
87
+ return emptyStatus(request.indexRoot);
88
+ }
89
+ throw error;
90
+ }
91
+ return store.query(request, signal);
92
+ }
93
+ async index(indexRoot, options = {}, signal) {
94
+ const indexer = await this.selectIndexer(indexRoot, signal);
95
+ return indexer.index(indexRoot, options, signal);
96
+ }
97
+ /**
98
+ * The one store that serves `indexRoot`. Every registered store is asked concurrently, so the
99
+ * answer does not depend on registration order; zero and several claimants both throw.
100
+ * @param indexRoot - directory the request targets.
101
+ * @param signal - aborts the availability checks.
102
+ * @returns the single claiming store.
103
+ */
104
+ async select(indexRoot, signal) {
105
+ const candidates = [...this.stores.values()];
106
+ if (candidates.length === 0) {
107
+ throw new SessiongraphError('no session-graph store is registered', 'SESSIONGRAPH_UNAVAILABLE');
108
+ }
109
+ const claims = await Promise.all(candidates.map(store => store.indexes(indexRoot, signal)));
110
+ const claimants = candidates.filter((_, index) => claims[index]);
111
+ const [only, rival] = claimants;
112
+ if (only === undefined) {
113
+ throw new SessiongraphError(`no session-graph store indexes "${indexRoot}"`, 'SESSIONGRAPH_UNAVAILABLE');
114
+ }
115
+ if (rival !== undefined) {
116
+ const ids = claimants.map(store => store.id).join(', ');
117
+ throw new SessiongraphError(`several session-graph stores index "${indexRoot}": ${ids}`, 'SESSIONGRAPH_CONFLICT');
118
+ }
119
+ return only;
120
+ }
121
+ /**
122
+ * The one indexer that can build `indexRoot`. Same one-claimant rule as {@link Sessiongraph.select}.
123
+ * @param indexRoot - directory to build into.
124
+ * @param signal - aborts the availability checks.
125
+ * @returns the single claiming indexer.
126
+ */
127
+ async selectIndexer(indexRoot, signal) {
128
+ const candidates = [...this.indexers.values()];
129
+ if (candidates.length === 0) {
130
+ throw new SessiongraphError('no session-graph indexer is registered', 'SESSIONGRAPH_UNAVAILABLE');
131
+ }
132
+ const claims = await Promise.all(candidates.map(indexer => indexer.canIndex(indexRoot, signal)));
133
+ const claimants = candidates.filter((_, index) => claims[index]);
134
+ const [only, rival] = claimants;
135
+ if (only === undefined) {
136
+ throw new SessiongraphError(`no session-graph indexer can index "${indexRoot}"`, 'SESSIONGRAPH_UNAVAILABLE');
137
+ }
138
+ if (rival !== undefined) {
139
+ const ids = claimants.map(indexer => indexer.id).join(', ');
140
+ throw new SessiongraphError(`several session-graph indexers can index "${indexRoot}": ${ids}`, 'SESSIONGRAPH_CONFLICT');
141
+ }
142
+ return only;
143
+ }
144
+ }
145
+ export default Sessiongraph;
146
+ /** Cordis plugin name for loader diagnostics. */
147
+ export const name = 'sessiongraph';
148
+ /**
149
+ * Publish the seam. Registers nothing else: providers arrive from their own plugins, which is what
150
+ * lets a deployment swap the store or the indexer without touching the model-facing tool.
151
+ * @param ctx - Cordis context to install into.
152
+ */
153
+ export function apply(ctx) {
154
+ ctx.plugin(Sessiongraph);
155
+ }
156
+ /**
157
+ * The zero-value status a root with no store claim reports: the index has never been built.
158
+ * @param indexRoot - directory the caller asked about.
159
+ * @returns a status result shaped exactly like a store's own, with nothing indexed.
160
+ */
161
+ function emptyStatus(indexRoot) {
162
+ return {
163
+ operation: 'status',
164
+ indexed: false,
165
+ indexPath: join(indexRoot, INDEX_RELATIVE_PATH),
166
+ sessions: 0,
167
+ entries: 0,
168
+ workspaces: 0,
169
+ indexedAt: undefined,
170
+ bytes: 0,
171
+ };
172
+ }
173
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-plugin-sessiongraph-service`.
3
+ * @module dsh-plugin-sessiongraph-service/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "sessiongraph-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-plugin-sessiongraph-service`.
3
+ * @module dsh-plugin-sessiongraph-service/invariant
4
+ */
5
+ const PACKAGE_NAME = 'dsh-plugin-sessiongraph-service';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'sessiongraph-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * No runtime invariant: store and indexer reservations are private state resolved per call, and the
12
+ * seam emits no lifecycle event and exposes no enumerable snapshot to compare an owned relation
13
+ * against.
14
+ */
15
+ const install = () => { };
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ /* jscpd:ignore-end */
23
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The on-disk schema both providers agree on. The indexer writes it; the store reads it. It lives in
3
+ * the seam package precisely so neither provider can drift from the other without editing this file.
4
+ *
5
+ * The format is ours to evolve: `SCHEMA_VERSION` is stamped into `meta`, and a store refuses a file
6
+ * whose version it does not know rather than guessing at a shape. That is deliberately stricter than
7
+ * "read whatever is there" — a mismatch must surface as a loud {@link SessiongraphError}, not as a
8
+ * silently empty result that looks like "no match".
9
+ * @module dsh-plugin-sessiongraph-service/schema
10
+ */
11
+ /** Current on-disk schema version. Bump on any change to {@link SCHEMA_STATEMENTS}. */
12
+ export declare const SCHEMA_VERSION = 1;
13
+ /** Relative path of the index file inside an index root. */
14
+ export declare const INDEX_RELATIVE_PATH = "index.db";
15
+ /** One session, keyed by its harness session id. */
16
+ export declare const SESSION_TABLE = "session";
17
+ /** One searchable entry inside a session. */
18
+ export declare const ENTRY_TABLE = "entry";
19
+ /** The FTS5 virtual table over `entry`, with `entry.id` as its `rowid`. */
20
+ export declare const ENTRY_FTS_TABLE = "entry_fts";
21
+ /**
22
+ * Every statement needed to create an index from nothing, in dependency order.
23
+ *
24
+ * `entry_fts` is an external-content-free FTS5 table: it stores only the bigram token stream and
25
+ * keys rows by `entry.id`, so the entry text is never duplicated into the index. `content=''` would
26
+ * also work, but the plain form keeps `delete`/`insert` symmetrical with one rowid.
27
+ */
28
+ export declare const SCHEMA_STATEMENTS: readonly string[];
29
+ /** `meta` keys the store and the indexer exchange through the file. */
30
+ export declare const META_SCHEMA_VERSION = "schema_version";
31
+ /** Epoch milliseconds of the last completed index run. */
32
+ export declare const META_INDEXED_AT = "indexed_at";
33
+ /** Number of source sessions the last run discovered. */
34
+ export declare const META_DISCOVERED = "discovered";
35
+ /** Number of source sessions the last run could not parse. */
36
+ export declare const META_FAILED = "failed";
37
+ //# sourceMappingURL=schema.d.ts.map
package/lib/schema.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The on-disk schema both providers agree on. The indexer writes it; the store reads it. It lives in
3
+ * the seam package precisely so neither provider can drift from the other without editing this file.
4
+ *
5
+ * The format is ours to evolve: `SCHEMA_VERSION` is stamped into `meta`, and a store refuses a file
6
+ * whose version it does not know rather than guessing at a shape. That is deliberately stricter than
7
+ * "read whatever is there" — a mismatch must surface as a loud {@link SessiongraphError}, not as a
8
+ * silently empty result that looks like "no match".
9
+ * @module dsh-plugin-sessiongraph-service/schema
10
+ */
11
+ /** Current on-disk schema version. Bump on any change to {@link SCHEMA_STATEMENTS}. */
12
+ export const SCHEMA_VERSION = 1;
13
+ /** Relative path of the index file inside an index root. */
14
+ export const INDEX_RELATIVE_PATH = 'index.db';
15
+ /** One session, keyed by its harness session id. */
16
+ export const SESSION_TABLE = 'session';
17
+ /** One searchable entry inside a session. */
18
+ export const ENTRY_TABLE = 'entry';
19
+ /** The FTS5 virtual table over `entry`, with `entry.id` as its `rowid`. */
20
+ export const ENTRY_FTS_TABLE = 'entry_fts';
21
+ /**
22
+ * Every statement needed to create an index from nothing, in dependency order.
23
+ *
24
+ * `entry_fts` is an external-content-free FTS5 table: it stores only the bigram token stream and
25
+ * keys rows by `entry.id`, so the entry text is never duplicated into the index. `content=''` would
26
+ * also work, but the plain form keeps `delete`/`insert` symmetrical with one rowid.
27
+ */
28
+ export const SCHEMA_STATEMENTS = [
29
+ `CREATE TABLE IF NOT EXISTS meta (
30
+ key TEXT PRIMARY KEY,
31
+ value TEXT NOT NULL
32
+ )`,
33
+ `CREATE TABLE IF NOT EXISTS ${SESSION_TABLE} (
34
+ id TEXT PRIMARY KEY,
35
+ workspace TEXT NOT NULL,
36
+ title TEXT,
37
+ created_at INTEGER NOT NULL,
38
+ last_at INTEGER NOT NULL,
39
+ parent_session TEXT,
40
+ origin TEXT,
41
+ agent_preset TEXT,
42
+ turns INTEGER NOT NULL DEFAULT 0,
43
+ entries INTEGER NOT NULL DEFAULT 0,
44
+ source_path TEXT NOT NULL,
45
+ source_mtime INTEGER NOT NULL,
46
+ source_size INTEGER NOT NULL
47
+ )`,
48
+ `CREATE TABLE IF NOT EXISTS ${ENTRY_TABLE} (
49
+ id INTEGER PRIMARY KEY,
50
+ session_id TEXT NOT NULL,
51
+ seq INTEGER NOT NULL,
52
+ time INTEGER NOT NULL,
53
+ turn INTEGER,
54
+ kind TEXT NOT NULL,
55
+ text TEXT NOT NULL,
56
+ clipped INTEGER NOT NULL DEFAULT 0
57
+ )`,
58
+ `CREATE INDEX IF NOT EXISTS idx_entry_session ON ${ENTRY_TABLE} (session_id, seq)`,
59
+ `CREATE INDEX IF NOT EXISTS idx_session_last ON ${SESSION_TABLE} (last_at DESC)`,
60
+ `CREATE INDEX IF NOT EXISTS idx_session_workspace ON ${SESSION_TABLE} (workspace, last_at DESC)`,
61
+ `CREATE VIRTUAL TABLE IF NOT EXISTS ${ENTRY_FTS_TABLE} USING fts5(
62
+ tokens,
63
+ tokenize = 'unicode61 remove_diacritics 0'
64
+ )`,
65
+ ];
66
+ /** `meta` keys the store and the indexer exchange through the file. */
67
+ export const META_SCHEMA_VERSION = 'schema_version';
68
+ /** Epoch milliseconds of the last completed index run. */
69
+ export const META_INDEXED_AT = 'indexed_at';
70
+ /** Number of source sessions the last run discovered. */
71
+ export const META_DISCOVERED = 'discovered';
72
+ /** Number of source sessions the last run could not parse. */
73
+ export const META_FAILED = 'failed';
74
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The one tokenizer both sides use: the indexer tokenizes an entry on write, the store tokenizes a
3
+ * query on read. Keeping them in the seam package is the whole point — a write-side and read-side
4
+ * tokenizer that drift by one rule produce an index that silently cannot answer its own queries.
5
+ *
6
+ * ## Why bigrams
7
+ *
8
+ * SQLite's `unicode61` tokenizer splits on characters that are neither alphanumeric nor `_`, and
9
+ * every CJK character is alphanumeric, so a whole Chinese sentence becomes ONE token: `广告` matches
10
+ * nothing in `我今天在改广告SDK`, verified against SQLite 3.50.4. FTS5's `trigram` tokenizer fixes
11
+ * three-or-more-character substrings but still misses the very common two-character Chinese word
12
+ * (`广告`, `域名`, `功率`). So Chinese runs are expanded into overlapping two-character tokens on
13
+ * BOTH sides: `广告点击` writes `广告 告点 点击`, and a query for `点击跳转` becomes
14
+ * `"点击" AND "跳转" AND ...`. Latin and digit runs stay whole words in lower case.
15
+ *
16
+ * The cost is a bigger token stream (roughly one token per Chinese character) and AND-semantics
17
+ * recall rather than exact phrase matching: `广告跳转` can match an entry where those characters are
18
+ * not adjacent. For "find the conversation where I talked about this", recall beats phrase fidelity,
19
+ * and a caller who needs adjacency can quote a longer term.
20
+ * @module dsh-plugin-sessiongraph-service/tokenize
21
+ */
22
+ /**
23
+ * Tokenize text into the stream written to `entry_fts.tokens` and compared against a query.
24
+ *
25
+ * @param text - raw entry text or query text.
26
+ * @returns tokens in document order, with duplicates kept (FTS5 deduplicates per position itself).
27
+ */
28
+ export declare function tokenize(text: string): string[];
29
+ /**
30
+ * Build the FTS5 MATCH expression for a user query.
31
+ *
32
+ * Tokens are double-quoted (so FTS5 treats them as literals rather than barewords) and joined with
33
+ * `AND`, which is the OR-free form that keeps a multi-token query narrowing instead of widening.
34
+ *
35
+ * @param query - raw query text.
36
+ * @returns an FTS5 MATCH expression, or `undefined` when the query has no searchable token.
37
+ */
38
+ export declare function matchExpression(query: string): string | undefined;
39
+ /**
40
+ * Tokenize a query the way the index expects, deduplicated, for snippet highlighting.
41
+ * @param query - raw query text.
42
+ * @returns distinct tokens, longest first so a longer term claims the highlight before a bigram does.
43
+ */
44
+ export declare function highlightTerms(query: string): string[];
45
+ /**
46
+ * Whether a character is a CJK ideograph.
47
+ * @param char - one character.
48
+ * @returns true for an ideograph.
49
+ */
50
+ export declare function isCjk(char: string): boolean;
51
+ //# sourceMappingURL=tokenize.d.ts.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The one tokenizer both sides use: the indexer tokenizes an entry on write, the store tokenizes a
3
+ * query on read. Keeping them in the seam package is the whole point — a write-side and read-side
4
+ * tokenizer that drift by one rule produce an index that silently cannot answer its own queries.
5
+ *
6
+ * ## Why bigrams
7
+ *
8
+ * SQLite's `unicode61` tokenizer splits on characters that are neither alphanumeric nor `_`, and
9
+ * every CJK character is alphanumeric, so a whole Chinese sentence becomes ONE token: `广告` matches
10
+ * nothing in `我今天在改广告SDK`, verified against SQLite 3.50.4. FTS5's `trigram` tokenizer fixes
11
+ * three-or-more-character substrings but still misses the very common two-character Chinese word
12
+ * (`广告`, `域名`, `功率`). So Chinese runs are expanded into overlapping two-character tokens on
13
+ * BOTH sides: `广告点击` writes `广告 告点 点击`, and a query for `点击跳转` becomes
14
+ * `"点击" AND "跳转" AND ...`. Latin and digit runs stay whole words in lower case.
15
+ *
16
+ * The cost is a bigger token stream (roughly one token per Chinese character) and AND-semantics
17
+ * recall rather than exact phrase matching: `广告跳转` can match an entry where those characters are
18
+ * not adjacent. For "find the conversation where I talked about this", recall beats phrase fidelity,
19
+ * and a caller who needs adjacency can quote a longer term.
20
+ * @module dsh-plugin-sessiongraph-service/tokenize
21
+ */
22
+ /** Longest Latin/digit run kept as one token before it is split on `_`/`-` boundaries. */
23
+ const MAX_LATIN_TOKEN_LENGTH = 64;
24
+ /** A CJK ideograph, including the extension-A block and the compatibility block. */
25
+ const CJK = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/;
26
+ /** A run of one or more CJK ideographs. */
27
+ const CJK_RUN = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]+/g;
28
+ /**
29
+ * A run of Latin letters and digits. `_` deliberately does NOT belong to the run: FTS5's `unicode61`
30
+ * treats it as a separator, and a caller searching `codegraph` must still find an entry that says
31
+ * `codegraph_index`, so the write side splits on it exactly like the tokenizer would.
32
+ */
33
+ const LATIN_RUN = /[A-Za-z0-9]+/g;
34
+ /**
35
+ * Tokenize text into the stream written to `entry_fts.tokens` and compared against a query.
36
+ *
37
+ * @param text - raw entry text or query text.
38
+ * @returns tokens in document order, with duplicates kept (FTS5 deduplicates per position itself).
39
+ */
40
+ export function tokenize(text) {
41
+ if (text === '')
42
+ return [];
43
+ const tokens = [];
44
+ // Walk the two character classes in one pass so a mixed string keeps its order; positions do not
45
+ // matter to the query, but a stable order keeps a written index byte-reproducible for tests.
46
+ const matched = [];
47
+ for (const match of text.matchAll(CJK_RUN)) {
48
+ matched.push({ at: match.index, value: bigrams(match[0]) });
49
+ }
50
+ for (const match of text.matchAll(LATIN_RUN)) {
51
+ const token = match[0].toLowerCase();
52
+ if (token.length === 0)
53
+ continue;
54
+ matched.push({ at: match.index, value: token.length > MAX_LATIN_TOKEN_LENGTH ? [token.slice(0, MAX_LATIN_TOKEN_LENGTH)] : [token] });
55
+ }
56
+ matched.sort((left, right) => left.at - right.at);
57
+ for (const entry of matched)
58
+ tokens.push(...entry.value);
59
+ return tokens;
60
+ }
61
+ /**
62
+ * Expand one CJK run into overlapping two-character tokens.
63
+ * @param run - a non-empty run of ideographs.
64
+ * @returns one token per character for a single character, else one per adjacent pair.
65
+ */
66
+ function bigrams(run) {
67
+ if (run.length <= 1)
68
+ return [run];
69
+ const tokens = [];
70
+ for (let index = 0; index < run.length - 1; index += 1)
71
+ tokens.push(run.slice(index, index + 2));
72
+ return tokens;
73
+ }
74
+ /**
75
+ * Build the FTS5 MATCH expression for a user query.
76
+ *
77
+ * Tokens are double-quoted (so FTS5 treats them as literals rather than barewords) and joined with
78
+ * `AND`, which is the OR-free form that keeps a multi-token query narrowing instead of widening.
79
+ *
80
+ * @param query - raw query text.
81
+ * @returns an FTS5 MATCH expression, or `undefined` when the query has no searchable token.
82
+ */
83
+ export function matchExpression(query) {
84
+ const tokens = dedupe(tokenize(query));
85
+ if (tokens.length === 0)
86
+ return undefined;
87
+ return tokens.map(token => `"${token.replaceAll('"', '""')}"`).join(' AND ');
88
+ }
89
+ /**
90
+ * Tokenize a query the way the index expects, deduplicated, for snippet highlighting.
91
+ * @param query - raw query text.
92
+ * @returns distinct tokens, longest first so a longer term claims the highlight before a bigram does.
93
+ */
94
+ export function highlightTerms(query) {
95
+ return dedupe(tokenize(query)).sort((left, right) => right.length - left.length);
96
+ }
97
+ /**
98
+ * Remove duplicate tokens while preserving first-seen order.
99
+ * @param tokens - tokens to dedupe.
100
+ * @returns distinct tokens.
101
+ */
102
+ function dedupe(tokens) {
103
+ return [...new Set(tokens)];
104
+ }
105
+ /**
106
+ * Whether a character is a CJK ideograph.
107
+ * @param char - one character.
108
+ * @returns true for an ideograph.
109
+ */
110
+ export function isCjk(char) {
111
+ return CJK.test(char);
112
+ }
113
+ //# sourceMappingURL=tokenize.js.map
package/lib/types.d.ts ADDED
@@ -0,0 +1,353 @@
1
+ /**
2
+ * The normalized vocabulary of the session-graph seam: what a caller may ask, what a store must
3
+ * answer, and what an indexer must report.
4
+ *
5
+ * Everything here is provider-neutral. A store ranges over one SQLite file today, but nothing in
6
+ * these types says so: `indexRoot` is an opaque directory the store claims, and a future store could
7
+ * serve it from anywhere. The seam therefore never exposes SQL, never exposes a schema version, and
8
+ * never exposes a path to a table — only sessions, entries, and search hits.
9
+ * @module dsh-plugin-sessiongraph-service/types
10
+ */
11
+ import type { SessiongraphIndexerId, SessiongraphSessionId, SessiongraphStoreId } from './brand.ts';
12
+ /** Every query operation the seam answers. */
13
+ export declare const SESSIONGRAPH_OPERATIONS: readonly ["search", "session", "timeline", "workspaces", "status"];
14
+ /** One query operation. */
15
+ export type SessiongraphOperation = (typeof SESSIONGRAPH_OPERATIONS)[number];
16
+ /**
17
+ * What a searchable entry is, from the reader's point of view.
18
+ *
19
+ * `user` is text a human actually typed; `assistant` is the model's visible reply (never its
20
+ * reasoning); `tool` is a tool invocation (its name and arguments, never the result body); `title`
21
+ * is a session title, kept as its own kind so a title match is recognizable in the hit list.
22
+ */
23
+ export declare const SESSIONGRAPH_ENTRY_KINDS: readonly ["user", "assistant", "tool", "title"];
24
+ /** One entry kind. */
25
+ export type SessiongraphEntryKind = (typeof SESSIONGRAPH_ENTRY_KINDS)[number];
26
+ /** Error codes the seam raises, so a consumer can distinguish "no index" from "bad request". */
27
+ export type SessiongraphErrorCode = 'SESSIONGRAPH_UNAVAILABLE' | 'SESSIONGRAPH_CONFLICT' | 'SESSIONGRAPH_INVALID_PROVIDER' | 'SESSIONGRAPH_INVALID_REQUEST' | 'SESSIONGRAPH_NOT_FOUND' | 'SESSIONGRAPH_CORRUPT' | 'SESSIONGRAPH_IO';
28
+ /** The single error type crossing the seam. */
29
+ export declare class SessiongraphError extends Error {
30
+ readonly code: SessiongraphErrorCode;
31
+ /**
32
+ * @param message - human-readable diagnosis.
33
+ * @param code - machine-readable category.
34
+ */
35
+ constructor(message: string, code: SessiongraphErrorCode);
36
+ }
37
+ /** Fields every query request carries. */
38
+ export interface SessiongraphRequestBase {
39
+ /** Directory holding the index this request targets. The seam routes on it; a store claims it. */
40
+ indexRoot: string;
41
+ }
42
+ /** Full-text search across indexed conversation history. */
43
+ export interface SessiongraphSearchRequest extends SessiongraphRequestBase {
44
+ operation: 'search';
45
+ /** Query text. Chinese is matched by character bigram, so a two-character term works. */
46
+ query: string;
47
+ /** Restrict to one workspace (exact `cwd`), or a path prefix when it ends without a trailing slash. */
48
+ workspace?: string;
49
+ /** Only entries at or after this epoch-millisecond timestamp. */
50
+ after?: number;
51
+ /** Only entries at or before this epoch-millisecond timestamp. */
52
+ before?: number;
53
+ /** Restrict to these entry kinds. Absent means every kind. */
54
+ kinds?: readonly SessiongraphEntryKind[];
55
+ /** Maximum hits to return. */
56
+ limit: number;
57
+ /** Hits to skip, for paging. */
58
+ offset?: number;
59
+ }
60
+ /** One matched entry, with enough context to recognize it without a second query. */
61
+ export interface SessiongraphHit {
62
+ sessionId: SessiongraphSessionId;
63
+ /** Session title, when the session ever produced one. */
64
+ title: string | undefined;
65
+ /** The session's working directory. */
66
+ workspace: string;
67
+ /** Entry timestamp, epoch milliseconds. */
68
+ time: number;
69
+ /** What matched. */
70
+ kind: SessiongraphEntryKind;
71
+ /** Event sequence number inside the session. */
72
+ seq: number;
73
+ /** Turn number inside the session, when the entry belongs to a turn. */
74
+ turn: number | undefined;
75
+ /** Matched text with the matched span wrapped in `[` `]`, truncated around the first match. */
76
+ snippet: string;
77
+ /** Total characters in the indexed entry, so a caller can tell a clip from the whole. */
78
+ length: number;
79
+ }
80
+ /** Answer to a search request. */
81
+ export interface SessiongraphSearchResult {
82
+ operation: 'search';
83
+ hits: SessiongraphHit[];
84
+ /** Total entries matching the query across the whole store, before `limit`/`offset`. */
85
+ total: number;
86
+ /** True when `total` exceeded `limit + offset`, or when a snippet was clipped. */
87
+ truncated: boolean;
88
+ }
89
+ /** A session's metadata, as the timeline and session operations report it. */
90
+ export interface SessiongraphSessionSummary {
91
+ sessionId: SessiongraphSessionId;
92
+ title: string | undefined;
93
+ workspace: string;
94
+ /** Epoch milliseconds of the session's first event. */
95
+ createdAt: number;
96
+ /** Epoch milliseconds of the session's last event. */
97
+ lastAt: number;
98
+ /** Number of turns the session contains. */
99
+ turns: number;
100
+ /** Number of indexed entries the session contributes. */
101
+ entries: number;
102
+ /** Parent session id, when this session was delegated from another. */
103
+ parentSession: string | undefined;
104
+ /** Session origin label (`user`, `subagent`, …), when the log records one. */
105
+ origin: string | undefined;
106
+ /** Agent preset the session ran under, when recorded. */
107
+ agentPreset: string | undefined;
108
+ }
109
+ /** One indexed entry, as the session operation reports it. */
110
+ export interface SessiongraphEntry {
111
+ seq: number;
112
+ time: number;
113
+ kind: SessiongraphEntryKind;
114
+ turn: number | undefined;
115
+ /** Entry text, already bounded by the indexer's per-entry cap. */
116
+ text: string;
117
+ /** True when the indexer stored a clipped prefix instead of the whole entry. */
118
+ clipped: boolean;
119
+ }
120
+ /** Read one session, by full id or by an unambiguous prefix. */
121
+ export interface SessiongraphSessionRequest extends SessiongraphRequestBase {
122
+ operation: 'session';
123
+ /** Full session id, or a prefix long enough to identify exactly one session. */
124
+ sessionId: string;
125
+ /** Maximum entries to return. */
126
+ limit: number;
127
+ /** Entries to skip, for paging; follows the session's own chronological order. */
128
+ offset?: number;
129
+ }
130
+ /** Answer to a session request. */
131
+ export interface SessiongraphSessionResult {
132
+ operation: 'session';
133
+ session: SessiongraphSessionSummary;
134
+ entries: SessiongraphEntry[];
135
+ /** Sessions delegated from this one, oldest first. */
136
+ children: SessiongraphSessionId[];
137
+ /** True when the session holds more entries than `limit + offset`. */
138
+ truncated: boolean;
139
+ }
140
+ /** List sessions by time. */
141
+ export interface SessiongraphTimelineRequest extends SessiongraphRequestBase {
142
+ operation: 'timeline';
143
+ /** Restrict to one workspace, or a path prefix when it ends without a trailing slash. */
144
+ workspace?: string;
145
+ /** Only sessions whose last event is at or after this epoch-millisecond timestamp. */
146
+ after?: number;
147
+ /** Only sessions whose first event is at or before this epoch-millisecond timestamp. */
148
+ before?: number;
149
+ /** Maximum sessions to return, newest first. */
150
+ limit: number;
151
+ }
152
+ /** Answer to a timeline request. */
153
+ export interface SessiongraphTimelineResult {
154
+ operation: 'timeline';
155
+ sessions: SessiongraphSessionSummary[];
156
+ /** True when more sessions match than were returned. */
157
+ truncated: boolean;
158
+ }
159
+ /** Enumerate the workspaces the index covers. */
160
+ export interface SessiongraphWorkspacesRequest extends SessiongraphRequestBase {
161
+ operation: 'workspaces';
162
+ /** Maximum workspaces to return, most recently active first. */
163
+ limit: number;
164
+ }
165
+ /** One workspace's coverage. */
166
+ export interface SessiongraphWorkspaceStat {
167
+ workspace: string;
168
+ sessions: number;
169
+ entries: number;
170
+ /** Epoch milliseconds of the oldest session in this workspace. */
171
+ firstAt: number;
172
+ /** Epoch milliseconds of the newest session in this workspace. */
173
+ lastAt: number;
174
+ }
175
+ /** Answer to a workspaces request. */
176
+ export interface SessiongraphWorkspacesResult {
177
+ operation: 'workspaces';
178
+ workspaces: SessiongraphWorkspaceStat[];
179
+ /** Total workspaces in the index, before `limit`. */
180
+ total: number;
181
+ }
182
+ /** Report the index's own state. Unlike every other operation, this one answers even when no index exists. */
183
+ export interface SessiongraphStatusRequest extends SessiongraphRequestBase {
184
+ operation: 'status';
185
+ }
186
+ /** Answer to a status request. */
187
+ export interface SessiongraphStatusResult {
188
+ operation: 'status';
189
+ /** False when `indexRoot` holds no index at all; every other field is then zero/empty. */
190
+ indexed: boolean;
191
+ /** Absolute path of the index file, for a caller that wants to inspect it directly. */
192
+ indexPath: string;
193
+ /** Sessions in the index. */
194
+ sessions: number;
195
+ /**
196
+ * Sessions whose source is the synthetic `live://<session-id>` marker rather than a log file:
197
+ * conversations that are still running and have not been written to disk yet. The count drops back
198
+ * to zero once each one ends and is re-indexed from its log. A store that cannot distinguish live
199
+ * sources omits the field; one that can always reports a number, `0` when every session is on disk.
200
+ */
201
+ liveSessions?: number;
202
+ /** Searchable entries in the index. */
203
+ entries: number;
204
+ /** Distinct workspaces in the index. */
205
+ workspaces: number;
206
+ /** Epoch milliseconds of the last successful index run, or undefined when never run. */
207
+ indexedAt: number | undefined;
208
+ /** Bytes the index file occupies on disk. */
209
+ bytes: number;
210
+ }
211
+ /** Every request the seam accepts, discriminated on `operation`. */
212
+ export type SessiongraphRequest = SessiongraphSearchRequest | SessiongraphSessionRequest | SessiongraphTimelineRequest | SessiongraphWorkspacesRequest | SessiongraphStatusRequest;
213
+ /** The result type a given request produces. */
214
+ export type SessiongraphResultFor<R extends SessiongraphRequest> = R extends SessiongraphSearchRequest ? SessiongraphSearchResult : R extends SessiongraphSessionRequest ? SessiongraphSessionResult : R extends SessiongraphTimelineRequest ? SessiongraphTimelineResult : R extends SessiongraphWorkspacesRequest ? SessiongraphWorkspacesResult : R extends SessiongraphStatusRequest ? SessiongraphStatusResult : never;
215
+ /** Every result the seam produces. */
216
+ export type SessiongraphResult = SessiongraphSearchResult | SessiongraphSessionResult | SessiongraphTimelineResult | SessiongraphWorkspacesResult | SessiongraphStatusResult;
217
+ /**
218
+ * A read provider: answers queries from an index it claims.
219
+ *
220
+ * `indexes` is asked for every registered store on every query, so it must be cheap (a stat, not a
221
+ * full open). Returning `false` is how a store declines a root it does not serve; a store never
222
+ * silently answers for a root it did not claim.
223
+ */
224
+ export interface SessiongraphStoreProvider {
225
+ /** Stable identity, unique among registered stores. */
226
+ readonly id: SessiongraphStoreId;
227
+ /**
228
+ * Whether this store can answer for `indexRoot`.
229
+ * @param indexRoot - directory the caller targeted.
230
+ * @param signal - aborts the check.
231
+ */
232
+ indexes(indexRoot: string, signal?: AbortSignal): Promise<boolean>;
233
+ /**
234
+ * Answer one query. The request's `operation` selects the shape; a store must answer every
235
+ * operation it can, and raise {@link SessiongraphError} rather than guess.
236
+ * @param request - the normalized request.
237
+ * @param signal - aborts the query.
238
+ */
239
+ query<R extends SessiongraphRequest>(request: R, signal?: AbortSignal): Promise<SessiongraphResultFor<R>>;
240
+ }
241
+ /** Options an indexer accepts for one run. */
242
+ export interface SessiongraphIndexOptions {
243
+ /**
244
+ * Rebuild every session from scratch. Without it a run skips sessions whose source file is
245
+ * unchanged since the last run.
246
+ */
247
+ force?: boolean;
248
+ /** Index only sessions whose `cwd` matches this workspace (exact, or a prefix without a trailing slash). */
249
+ workspace?: string;
250
+ /** Index only one session, by full id or unambiguous prefix. */
251
+ sessionId?: string;
252
+ }
253
+ /** What one index run did. */
254
+ export interface SessiongraphIndexReport {
255
+ /** Absolute path of the index the run wrote. */
256
+ indexPath: string;
257
+ /** Sessions added since the previous run. */
258
+ added: number;
259
+ /** Sessions whose source changed and were re-written. */
260
+ updated: number;
261
+ /** Sessions skipped because their source was unchanged. */
262
+ skipped: number;
263
+ /** Sessions dropped because their source file disappeared. */
264
+ removed: number;
265
+ /**
266
+ * Sessions rebuilt from the host's live (still-running) view this run. They belong to no source
267
+ * file yet, are replayed in full on every run rather than compared by version, and are therefore
268
+ * not counted in `added`/`updated`/`skipped`.
269
+ */
270
+ liveSessions?: number;
271
+ /** Live sessions left out because they exceeded the indexer's configured cap. */
272
+ liveSkipped?: number;
273
+ /**
274
+ * Engine that produced this run's totals: the native Rust kernel or the TypeScript indexer. Absent
275
+ * on reports from a build that predates engine selection.
276
+ */
277
+ engine?: 'native' | 'typescript';
278
+ /** Sessions discovered in total across all source roots. */
279
+ discovered: number;
280
+ /** Sessions that failed to parse, with a bounded reason. */
281
+ failed: {
282
+ path: string;
283
+ reason: string;
284
+ }[];
285
+ /** Searchable entries written. */
286
+ entries: number;
287
+ /** Sessions in the index after the run. */
288
+ sessions: number;
289
+ /** Distinct workspaces in the index after the run. */
290
+ workspaces: number;
291
+ /** Bytes the index occupies after the run. */
292
+ bytes: number;
293
+ /** Wall-clock duration of the run, milliseconds. */
294
+ elapsedMs: number;
295
+ }
296
+ /**
297
+ * A write provider: parses session logs and builds the index a store reads.
298
+ *
299
+ * Indexing is never triggered from {@link SessiongraphService.query}. It is a caller-initiated,
300
+ * potentially slow operation on its own timeout budget.
301
+ */
302
+ export interface SessiongraphIndexer {
303
+ /** Stable identity, unique among registered indexers. */
304
+ readonly id: SessiongraphIndexerId;
305
+ /**
306
+ * Whether this indexer can build an index under `indexRoot`.
307
+ * @param indexRoot - directory the caller targeted.
308
+ * @param signal - aborts the check.
309
+ */
310
+ canIndex(indexRoot: string, signal?: AbortSignal): Promise<boolean>;
311
+ /**
312
+ * Build or refresh the index.
313
+ * @param indexRoot - directory to write into.
314
+ * @param options - scope and rebuild switches.
315
+ * @param signal - aborts the run; a store must leave the previous index readable.
316
+ */
317
+ index(indexRoot: string, options: SessiongraphIndexOptions, signal?: AbortSignal): Promise<SessiongraphIndexReport>;
318
+ }
319
+ /** The facade `ctx.sessiongraph` publishes. */
320
+ export interface SessiongraphService {
321
+ /**
322
+ * Register a read provider.
323
+ * @param provider - the store to publish.
324
+ * @returns a disposer that unregisters it.
325
+ */
326
+ registerStore(provider: SessiongraphStoreProvider): () => void;
327
+ /**
328
+ * Register a write provider.
329
+ * @param provider - the indexer to publish.
330
+ * @returns a disposer that unregisters it.
331
+ */
332
+ registerIndexer(provider: SessiongraphIndexer): () => void;
333
+ /**
334
+ * Whether any store can answer for `indexRoot`.
335
+ * @param indexRoot - directory to test.
336
+ * @param signal - aborts the check.
337
+ */
338
+ available(indexRoot: string, signal?: AbortSignal): Promise<boolean>;
339
+ /**
340
+ * Answer one query through the single store claiming the request's `indexRoot`.
341
+ * @param request - the normalized request.
342
+ * @param signal - aborts the query.
343
+ */
344
+ query<R extends SessiongraphRequest>(request: R, signal?: AbortSignal): Promise<SessiongraphResultFor<R>>;
345
+ /**
346
+ * Run the single indexer claiming `indexRoot`.
347
+ * @param indexRoot - directory to build into.
348
+ * @param options - scope and rebuild switches.
349
+ * @param signal - aborts the run.
350
+ */
351
+ index(indexRoot: string, options?: SessiongraphIndexOptions, signal?: AbortSignal): Promise<SessiongraphIndexReport>;
352
+ }
353
+ //# sourceMappingURL=types.d.ts.map
package/lib/types.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The normalized vocabulary of the session-graph seam: what a caller may ask, what a store must
3
+ * answer, and what an indexer must report.
4
+ *
5
+ * Everything here is provider-neutral. A store ranges over one SQLite file today, but nothing in
6
+ * these types says so: `indexRoot` is an opaque directory the store claims, and a future store could
7
+ * serve it from anywhere. The seam therefore never exposes SQL, never exposes a schema version, and
8
+ * never exposes a path to a table — only sessions, entries, and search hits.
9
+ * @module dsh-plugin-sessiongraph-service/types
10
+ */
11
+ /** Every query operation the seam answers. */
12
+ export const SESSIONGRAPH_OPERATIONS = ['search', 'session', 'timeline', 'workspaces', 'status'];
13
+ /**
14
+ * What a searchable entry is, from the reader's point of view.
15
+ *
16
+ * `user` is text a human actually typed; `assistant` is the model's visible reply (never its
17
+ * reasoning); `tool` is a tool invocation (its name and arguments, never the result body); `title`
18
+ * is a session title, kept as its own kind so a title match is recognizable in the hit list.
19
+ */
20
+ export const SESSIONGRAPH_ENTRY_KINDS = ['user', 'assistant', 'tool', 'title'];
21
+ /** The single error type crossing the seam. */
22
+ export class SessiongraphError extends Error {
23
+ code;
24
+ /**
25
+ * @param message - human-readable diagnosis.
26
+ * @param code - machine-readable category.
27
+ */
28
+ constructor(message, code) {
29
+ super(message);
30
+ this.code = code;
31
+ this.name = 'SessiongraphError';
32
+ }
33
+ }
34
+ //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "dsh-plugin-sessiongraph-service",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Service definition for the DeepSeek Harness session-graph seam (ctx.sessiongraph) — a store-provider and indexer-provider registry over local conversation history, plus the normalized query vocabulary the model-facing tool speaks",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "deepseek-harness",
9
+ "cordis",
10
+ "cordis-plugin",
11
+ "session",
12
+ "session-history",
13
+ "search",
14
+ "fts",
15
+ "conversation",
16
+ "ai-agent"
17
+ ],
18
+ "license": "MIT",
19
+ "author": "CC ZHAO",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/CC19990113/dsh-plugin-sessiongraph.git",
23
+ "directory": "packages/service"
24
+ },
25
+ "homepage": "https://github.com/CC19990113/dsh-plugin-sessiongraph#readme",
26
+ "bugs": "https://github.com/CC19990113/dsh-plugin-sessiongraph/issues",
27
+ "type": "module",
28
+ "main": "lib/index.js",
29
+ "types": "lib/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./lib/index.d.ts",
33
+ "default": "./lib/index.js"
34
+ },
35
+ "./invariant": {
36
+ "types": "./lib/invariant.d.ts",
37
+ "default": "./lib/invariant.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "lib/**/*.js",
43
+ "lib/**/*.d.ts"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "provenance": false
48
+ },
49
+ "dependencies": {
50
+ "@deepseek-ai/schemastery": "^3.18.1"
51
+ },
52
+ "peerDependencies": {
53
+ "@deepseek-ai/cordis": ">=4.0.1",
54
+ "@deepseek-ai/dsh-invariants": ">=0.1.0-rc.6",
55
+ "@deepseek-ai/dsh-llm": ">=0.1.0-rc.6"
56
+ },
57
+ "devDependencies": {
58
+ "@deepseek-ai/cordis": "^4.0.1",
59
+ "@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
60
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.6"
61
+ }
6
62
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.