@yadsh/dsh-qa-surface 0.7.3 → 0.8.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 (146) hide show
  1. package/README.md +56 -9
  2. package/lib/access/model.js +24 -4
  3. package/lib/access/model.js.map +1 -1
  4. package/lib/access/role-repository.js +199 -91
  5. package/lib/access/role-repository.js.map +1 -1
  6. package/lib/access/service.js +194 -6
  7. package/lib/access/service.js.map +1 -1
  8. package/lib/account-remotes.js +2 -4
  9. package/lib/account-remotes.js.map +1 -1
  10. package/lib/accounts/database.js +447 -0
  11. package/lib/accounts/database.js.map +1 -0
  12. package/lib/accounts/file.js +27 -90
  13. package/lib/accounts/file.js.map +1 -1
  14. package/lib/accounts/store.js +210 -59
  15. package/lib/accounts/store.js.map +1 -1
  16. package/lib/admin/quality-store.js +199 -54
  17. package/lib/admin/quality-store.js.map +1 -1
  18. package/lib/admin/service.js +39 -6
  19. package/lib/admin/service.js.map +1 -1
  20. package/lib/cli.js +2 -1
  21. package/lib/cli.js.map +1 -1
  22. package/lib/client/QaSessionController.js +20 -2
  23. package/lib/client/QaSessionController.js.map +1 -1
  24. package/lib/client/QaSurface.js +69 -31
  25. package/lib/client/QaSurface.js.map +1 -1
  26. package/lib/client/QaTranscriptAdapter.js +4 -0
  27. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  28. package/lib/client/admin/QaAdmin.js +17 -7
  29. package/lib/client/admin/QaAdmin.js.map +1 -1
  30. package/lib/client/admin/{format.js → copy.js} +1 -1
  31. package/lib/client/admin/copy.js.map +1 -0
  32. package/lib/client/admin/pages/Conversations.js +1 -1
  33. package/lib/client/admin/pages/Conversations.js.map +1 -1
  34. package/lib/client/admin/pages/Overview.js +1 -1
  35. package/lib/client/admin/pages/Overview.js.map +1 -1
  36. package/lib/client/admin/pages/Quality.js +1 -1
  37. package/lib/client/admin/pages/Quality.js.map +1 -1
  38. package/lib/client/admin/pages/Review.js +1 -1
  39. package/lib/client/admin/pages/Review.js.map +1 -1
  40. package/lib/client/admin/pages/Users.js +6 -2
  41. package/lib/client/admin/pages/Users.js.map +1 -1
  42. package/lib/client/admin/shared.js +1 -1
  43. package/lib/client/admin/shared.js.map +1 -1
  44. package/lib/client/components/Markdown.js +14 -206
  45. package/lib/client/components/Markdown.js.map +1 -1
  46. package/lib/client/components/QaChangelog.js +60 -0
  47. package/lib/client/components/QaChangelog.js.map +1 -1
  48. package/lib/client/components/QaHeader.js +1 -3
  49. package/lib/client/components/QaHeader.js.map +1 -1
  50. package/lib/client/components/QaMessage.js +9 -4
  51. package/lib/client/components/QaMessage.js.map +1 -1
  52. package/lib/client/components/QaSidebar.js +5 -2
  53. package/lib/client/components/QaSidebar.js.map +1 -1
  54. package/lib/client/components/QaSourcesPanel.js +3 -18
  55. package/lib/client/components/QaSourcesPanel.js.map +1 -1
  56. package/lib/client/components/QaWorkGroup.js +4 -8
  57. package/lib/client/components/QaWorkGroup.js.map +1 -1
  58. package/lib/client/components/source-icon.js +26 -0
  59. package/lib/client/components/source-icon.js.map +1 -0
  60. package/lib/client/components/thinking-phrases.js +2 -11
  61. package/lib/client/components/thinking-phrases.js.map +1 -1
  62. package/lib/client/components/use-now.js +21 -0
  63. package/lib/client/components/use-now.js.map +1 -0
  64. package/lib/client/index.js +11 -3
  65. package/lib/client/index.js.map +1 -1
  66. package/lib/client/lineage.js +18 -0
  67. package/lib/client/lineage.js.map +1 -0
  68. package/lib/client/markdown/CodeBlock.js +28 -0
  69. package/lib/client/markdown/CodeBlock.js.map +1 -0
  70. package/lib/client/markdown/blocks.js +342 -0
  71. package/lib/client/markdown/blocks.js.map +1 -0
  72. package/lib/client/markdown/highlight.js +1089 -0
  73. package/lib/client/markdown/highlight.js.map +1 -0
  74. package/lib/client/markdown/inline.js +377 -0
  75. package/lib/client/markdown/inline.js.map +1 -0
  76. package/lib/client/markdown/render.js +147 -0
  77. package/lib/client/markdown/render.js.map +1 -0
  78. package/lib/client/markdown/source-chip.js +55 -0
  79. package/lib/client/markdown/source-chip.js.map +1 -0
  80. package/lib/client/markdown/use-transient-flag.js +27 -0
  81. package/lib/client/markdown/use-transient-flag.js.map +1 -0
  82. package/lib/client/role/RoleSelector.js +11 -2
  83. package/lib/client/role/RoleSelector.js.map +1 -1
  84. package/lib/client/role/preview.js +123 -0
  85. package/lib/client/role/preview.js.map +1 -0
  86. package/lib/client/styles.js +73 -19
  87. package/lib/client/styles.js.map +1 -1
  88. package/lib/client.js +3137 -761
  89. package/lib/client.js.map +1 -1
  90. package/lib/config-resolvers/accounts.js +15 -0
  91. package/lib/config-resolvers/accounts.js.map +1 -1
  92. package/lib/config-resolvers/defaults.js +8 -0
  93. package/lib/config-resolvers/defaults.js.map +1 -1
  94. package/lib/config-resolvers/sources.js +16 -0
  95. package/lib/config-resolvers/sources.js.map +1 -1
  96. package/lib/config.js +57 -0
  97. package/lib/config.js.map +1 -1
  98. package/lib/index.js +25 -10
  99. package/lib/index.js.map +1 -1
  100. package/lib/lockdown-policy.js +19 -0
  101. package/lib/lockdown-policy.js.map +1 -1
  102. package/lib/provenance/host-store.js +7 -3
  103. package/lib/provenance/host-store.js.map +1 -1
  104. package/lib/provenance/retention.js +25 -0
  105. package/lib/provenance/retention.js.map +1 -0
  106. package/lib/provenance/snapshot-store.js +290 -35
  107. package/lib/provenance/snapshot-store.js.map +1 -1
  108. package/lib/questions.js +16 -3
  109. package/lib/questions.js.map +1 -1
  110. package/lib/secure-session.js +136 -23
  111. package/lib/secure-session.js.map +1 -1
  112. package/lib/typert.host.js +79 -52
  113. package/lib/typert.remote-client.js +75 -48
  114. package/lib/types/access/role-repository.d.ts +38 -7
  115. package/lib/types/access/service.d.ts +77 -3
  116. package/lib/types/account-remotes.d.ts +8 -4
  117. package/lib/types/accounts/database.d.ts +84 -0
  118. package/lib/types/accounts/file.d.ts +17 -39
  119. package/lib/types/accounts/store.d.ts +98 -19
  120. package/lib/types/admin/quality-store.d.ts +20 -2
  121. package/lib/types/client/admin/{format.d.ts → copy.d.ts} +1 -1
  122. package/lib/types/client/components/Markdown.d.ts +7 -8
  123. package/lib/types/client/components/QaSidebar.d.ts +3 -1
  124. package/lib/types/client/components/QaSourcesPanel.d.ts +1 -3
  125. package/lib/types/client/components/source-icon.d.ts +12 -0
  126. package/lib/types/client/components/use-now.d.ts +8 -0
  127. package/lib/types/client/lineage.d.ts +15 -0
  128. package/lib/types/client/markdown/CodeBlock.d.ts +14 -0
  129. package/lib/types/client/markdown/blocks.d.ts +74 -0
  130. package/lib/types/client/markdown/highlight.d.ts +29 -0
  131. package/lib/types/client/markdown/inline.d.ts +40 -0
  132. package/lib/types/client/markdown/render.d.ts +13 -0
  133. package/lib/types/client/markdown/source-chip.d.ts +32 -0
  134. package/lib/types/client/markdown/use-transient-flag.d.ts +12 -0
  135. package/lib/types/client/role/RoleSelector.d.ts +11 -1
  136. package/lib/types/client/role/preview.d.ts +109 -0
  137. package/lib/types/index.d.ts +5 -1
  138. package/lib/types/lockdown-policy.d.ts +16 -0
  139. package/lib/types/provenance/host-store.d.ts +7 -3
  140. package/lib/types/provenance/retention.d.ts +46 -0
  141. package/lib/types/provenance/snapshot-store.d.ts +63 -7
  142. package/lib/types/questions.d.ts +5 -0
  143. package/lib/types/secure-session.d.ts +27 -1
  144. package/lib/types/types.d.ts +54 -0
  145. package/package.json +3 -3
  146. package/lib/client/admin/format.js.map +0 -1
@@ -3,7 +3,8 @@ import type { Context } from "@deepseek-ai/cordis";
3
3
  import type { PluginLogger } from "@yadsh/dsh-plugin-log";
4
4
  import type { QaAccounts } from "../accounts/store.js";
5
5
  import type { QaAgentToolGrants } from "../enforcement/tool-grants.js";
6
- import type { QaAccessAdminSnapshot, QaCapabilityConfig, QaCapabilityDescriptor, QaCapabilitySelection, QaCurrentAccess, QaEffectiveCapabilityPolicy, QaSessionAccess, QaSkillActivationRecord, QaSkillAssignmentOverride, QaSubrole, QaUserAccess, ResolvedQaSurfaceConfig } from "../types.js";
6
+ import type { QaAccessAdminSnapshot, QaCapabilityConfig, QaCapabilityDescriptor, QaCapabilitySelection, QaClaimResult, QaCurrentAccess, QaEffectiveCapabilityPolicy, QaSessionAccess, QaSkillActivationRecord, QaSkillAssignmentOverride, QaSubrole, QaUserAccess, ResolvedQaSurfaceConfig } from "../types.js";
7
+ import type { QaSessionLogReader } from "../admin/session-log.js";
7
8
  import { QaCapabilityCatalog, type CapabilityCatalogSnapshot } from "./capability-catalog.js";
8
9
  import { QaRoleRepository } from "./role-repository.js";
9
10
  export interface QaResolvedSessionPolicy {
@@ -21,16 +22,81 @@ export interface QaResolvedSessionPolicy {
21
22
  }
22
23
  /** Server-only orchestration of roles, assignments, discovery and snapshots. */
23
24
  export declare class QaAccessService {
25
+ private readonly ctx;
24
26
  private readonly options;
25
27
  readonly roles: QaRoleRepository;
26
28
  readonly catalog: QaCapabilityCatalog;
29
+ private lastOwnershipSweepAt;
30
+ /**
31
+ * Sessions the Host has listed as delegated children of another session.
32
+ * The live registry answers for a child that is running right now; this
33
+ * cache answers for the ones that finished and were dismantled, which is the
34
+ * state a browser meets when it opens an old subagent transcript.
35
+ */
36
+ private readonly delegatedSessions;
37
+ private lastDelegatedSweepAt;
38
+ private delegatedSweep;
27
39
  constructor(ctx: Context, options: {
28
40
  readonly accounts: () => QaAccounts | undefined;
29
41
  readonly config: () => ResolvedQaSurfaceConfig;
30
42
  readonly logger: PluginLogger;
31
43
  readonly repository?: QaRoleRepository;
32
44
  readonly dynamicToolNames?: () => readonly string[];
45
+ /**
46
+ * Durable session listing, used to tell a chat from a delegated child
47
+ * across Host runs. Without it the lineage answer degrades to the live
48
+ * registry, which knows nothing about a session this process has not
49
+ * materialized — the sweep then reclaims nothing rather than guessing.
50
+ */
51
+ readonly sessionLog?: QaSessionLogReader;
33
52
  });
53
+ /**
54
+ * Whether the Host can prove this session is a delegated child: a subagent's
55
+ * session, which is an implementation detail of one answer and never a chat.
56
+ * Used to refuse one wherever conversations are listed or claimed.
57
+ */
58
+ isDelegatedChild(sessionId: string): boolean;
59
+ /**
60
+ * The Host-registered facts of one session, for the accounts store's
61
+ * bounded auto-claim. Absent facts (the session registry is unavailable,
62
+ * or this chat is not materialized here) keep the historical claim.
63
+ */
64
+ private sessionFacts;
65
+ /**
66
+ * Reclaim ownership records of chats the Harness no longer knows.
67
+ *
68
+ * Nothing else removes them: the browser's "delete chat" hides a chat in one
69
+ * browser, and a record outlives its session forever, so a year of QA traffic
70
+ * accumulates one row per chat ever created. The sweep is throttled, runs
71
+ * off the reservation path, and is skipped entirely when the Harness cannot
72
+ * say what exists — an unanswerable question is never a licence to delete an
73
+ * auth boundary.
74
+ */
75
+ private maybePruneOwnership;
76
+ /**
77
+ * Migrate a browser's local chat index into server-side ownership.
78
+ *
79
+ * A delegated session is not a chat: claiming one would write exactly the
80
+ * ownership record the admission refuses to create and put a subagent's
81
+ * transcript in the account's chat list. Its ids are left out of the batch
82
+ * and reported, so a stale browser index cannot reintroduce the record.
83
+ */
84
+ claimSessions(token: string, sessionIds: readonly string[]): QaClaimResult;
85
+ /**
86
+ * Reclaim ownership records that point at delegated sessions, and remember
87
+ * which sessions are children while doing it.
88
+ *
89
+ * Records like this are residue of the paths that used to claim a session
90
+ * without knowing its lineage: each one keeps a subagent's transcript in the
91
+ * chat list of whoever opened it. Only positively identified children lose
92
+ * their record, so a listing that cannot be read reclaims nothing, and every
93
+ * other record stays the auth boundary it is.
94
+ *
95
+ * @returns the sweep, for callers that want to await it (tests).
96
+ */
97
+ reclaimDelegatedSessions(): Promise<void>;
98
+ /** Throttled, off the critical path, and never awaited by a request. */
99
+ private maybeSweepDelegatedSessions;
34
100
  current(token: string): QaCurrentAccess;
35
101
  session(token: string, sessionId: string): QaSessionAccess;
36
102
  reserveSession(token: string, sessionId: string, requestedSubrole: string | null, adminPreview: boolean): import("../types.js").QaAccountUserPublic;
@@ -47,9 +113,17 @@ export declare class QaAccessService {
47
113
  deleteSubrole(token: string, id: string, replacementId: string | null): void;
48
114
  updateCommon(token: string, input: QaCapabilitySelection): QaCapabilitySelection;
49
115
  updateAssignment(token: string, userId: string, input: QaUserAccess): QaUserAccess;
116
+ /** The accounts store, or the refusal this operation names. */
50
117
  private requireAccounts;
51
- /** The activation diagnostic is runtime plumbing, not an administrator grant. */
52
- private systemRequiredTools;
118
+ /**
119
+ * Tools every session resolves regardless of the role it runs under: the
120
+ * deployment's pinned allow-list plus the activation diagnostic.
121
+ *
122
+ * Readable outside the service because the administration surface reports a
123
+ * profile's effective capabilities next to the configured ones, and the
124
+ * pinned set is part of that answer whatever the profile says.
125
+ */
126
+ systemRequiredTools(): readonly string[];
53
127
  private requireAdmin;
54
128
  }
55
129
  export declare function validateCapabilityConfig(config: QaCapabilityConfig): QaCapabilityConfig;
@@ -1,6 +1,6 @@
1
1
  import type { PluginLogger } from "@yadsh/dsh-plugin-log";
2
2
  import { QaAccounts } from "./accounts/store.js";
3
- import type { QaAccountProfileInput, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaClaimResult, QaOwnershipEntry, QaWhoamiResult, ResolvedQaSurfaceConfig } from "./types.js";
3
+ import type { QaAccountProfileInput, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaOwnershipEntry, QaWhoamiResult, ResolvedQaSurfaceConfig } from "./types.js";
4
4
  /**
5
5
  * Host-side bodies of the account remotes the `QaSurface` service exposes.
6
6
  * The `@Remote`-decorated signatures stay on the service class (typert code
@@ -23,9 +23,13 @@ export interface QaAccountRemotes {
23
23
  login(email: string, password: string): QaAccountSession;
24
24
  /** Identity probe; safe to call with an empty or expired token. */
25
25
  whoami(token: string): QaWhoamiResult;
26
- /** Migrate a browser's local chat index into server-side ownership. */
27
- claimSessions(token: string, sessionIds: readonly string[]): QaClaimResult;
28
- /** The token user's owned session ids; the sidebar list authority. */
26
+ /**
27
+ * The token user's owned session ids; the sidebar list authority.
28
+ *
29
+ * Migrating a browser's local chat index is NOT here: it has to refuse a
30
+ * delegated session, which takes session lineage, and that lives in
31
+ * `QaAccessService.claimSessions` — one entry point, no path around it.
32
+ */
29
33
  ownedSessions(token: string): {
30
34
  readonly ids: readonly string[];
31
35
  };
@@ -0,0 +1,84 @@
1
+ import { SqliteDatabase } from "@yadsh/dsh-plugin-kit/sqlite";
2
+ import type { QaSkillActivationRecord } from "../types.js";
3
+ import { type AccountsFile, type StoredOwnership, type StoredUser } from "./file.js";
4
+ /** One ownership record as it is written, before it becomes a row. */
5
+ export interface OwnershipWrite {
6
+ readonly sessionId: string;
7
+ readonly owner: StoredOwnership;
8
+ }
9
+ /**
10
+ * The deployment's accounts, as tables.
11
+ *
12
+ * The stores this replaces were one JSON document read, parsed and rewritten
13
+ * whole on every mutation — so a chat being opened rewrote every account and
14
+ * every chat the deployment had ever seen, and the cost of that write grew with
15
+ * the deployment's age. Here a mutation touches the row it changes: opening a
16
+ * chat inserts one ownership row, and the file's size stops being a per-turn
17
+ * cost.
18
+ *
19
+ * Cross-process freshness survives the change. The `qa-accounts` CLI writes the
20
+ * same database from a second process, so the in-memory model is invalidated by
21
+ * `PRAGMA data_version`, which SQLite bumps exactly when *another* connection
22
+ * commits — the same guarantee the previous mtime-and-size stamp provided, with
23
+ * no file read to notice it.
24
+ */
25
+ export declare class QaAccountsDatabase {
26
+ readonly filePath: string;
27
+ readonly storage: SqliteDatabase;
28
+ private observedDataVersion;
29
+ constructor(filePath: string);
30
+ /** The token secret, creating one for a database that has none yet. */
31
+ get secret(): string;
32
+ /** True while the in-memory model a caller holds is still current. */
33
+ get unchangedSinceLoad(): boolean;
34
+ /** Take the current data version as the baseline a caller's model reflects. */
35
+ markLoaded(): void;
36
+ /** Every account, ownership record and skill activation, in one read. */
37
+ loadAll(): AccountsFile;
38
+ /** Append one account, preserving the order accounts are listed in. */
39
+ insertUser(user: StoredUser): void;
40
+ /** Replace one account's row in place. */
41
+ updateUser(user: StoredUser): void;
42
+ /** Record a successful sign-in without rewriting the account. */
43
+ touchLogin(userId: string, at: string): void;
44
+ /** Claim one chat for an account. */
45
+ insertOwnership(sessionId: string, owner: StoredOwnership): void;
46
+ /** Claim many chats at once, which is how a browser's index migrates. */
47
+ insertOwnershipMany(rows: readonly OwnershipWrite[]): void;
48
+ /** Rewrite an existing chat's pinned role, preview flag or snapshot. */
49
+ updateOwnership(sessionId: string, owner: StoredOwnership): void;
50
+ /** Forget chats: a rolled-back reservation, or a chat the Harness lost. */
51
+ deleteOwnership(sessionIds: readonly string[]): void;
52
+ /**
53
+ * Append one skill activation and keep the newest `limit` of them. The list
54
+ * is bounded because a long session may activate skills many times, and the
55
+ * oldest entries stop being interesting long before the store does.
56
+ */
57
+ appendSkillActivation(sessionId: string, entry: QaSkillActivationRecord, limit: number): readonly QaSkillActivationRecord[];
58
+ /**
59
+ * Import a pre-SQLite `qa-accounts.json` exactly once, then rename it away.
60
+ *
61
+ * The import is verified *inside* the transaction and before the file is
62
+ * retired — every account and every ownership record must have arrived, and
63
+ * the token secret must be the one the file carried — because a silent
64
+ * partial import would either lock accounts out or hand a chat to the wrong
65
+ * owner. A database that already holds accounts is never overwritten by a
66
+ * leftover file.
67
+ */
68
+ importLegacyFile(legacyFilePath: string | undefined): AccountsFile | undefined;
69
+ private assertImportArrived;
70
+ close(): void;
71
+ private hasAccounts;
72
+ private readDataVersion;
73
+ private ensureSecret;
74
+ /** The account columns, in the order both write statements bind them. */
75
+ private accountColumns;
76
+ private writeUser;
77
+ private writeOwnership;
78
+ /** One row per distinct policy; the digest is the policy's content. */
79
+ private writeSnapshot;
80
+ /** Drop snapshots no chat refers to any more, so the table cannot drift up. */
81
+ private gcSnapshots;
82
+ private parseLegacyFile;
83
+ }
84
+ //# sourceMappingURL=database.d.ts.map
@@ -1,13 +1,12 @@
1
- import type { Stats } from "node:fs";
2
1
  import type { QaAccountRole, QaEffectiveCapabilityPolicy, QaSkillActivationRecord, QaUserAccess } from "../types.js";
3
- /** One account's self-declared profile as it rests in the accounts file. */
2
+ /** One account's self-declared profile as it rests in the accounts store. */
4
3
  export interface StoredProfile {
5
4
  fullName?: string;
6
5
  identities?: Record<string, string>;
7
6
  instructions?: string;
8
7
  updatedAt?: string;
9
8
  }
10
- /** One account's starter buttons as they rest in the accounts file. */
9
+ /** One account's starter buttons as they rest in the accounts store. */
11
10
  export interface StoredStarters {
12
11
  items?: {
13
12
  label?: string;
@@ -53,51 +52,30 @@ export interface StoredOwnership {
53
52
  /** Skill activations in order: what the model gained, and when it gained it. */
54
53
  readonly skillActivations?: readonly QaSkillActivationRecord[];
55
54
  }
55
+ /**
56
+ * The accounts model as the store holds it in memory, hydrated from the
57
+ * database and invalidated by `PRAGMA data_version` when another process
58
+ * writes. Snapshots appear inline here: the storage layer is what keeps one
59
+ * copy per distinct policy, not the callers.
60
+ */
56
61
  export interface AccountsFile {
57
62
  readonly version: 1;
58
- /** HMAC key for account tokens; rotated on password change by rewriting it. */
63
+ /** HMAC key for account tokens; created on first use. */
59
64
  readonly secret: string;
60
65
  readonly users: StoredUser[];
61
66
  readonly ownership: Record<string, StoredOwnership>;
62
67
  }
63
- /** mtime+size pair identifying one on-disk version of the accounts file. */
64
- export interface FileStamp {
65
- readonly mtimeMs: number;
66
- readonly size: number;
67
- }
68
- /**
69
- * Where the last-seen stamp lives. Load and persist keep it current in place,
70
- * so a caller holds one mutable ref instead of threading the stamp through
71
- * every read-modify-write.
72
- */
73
- export interface FileStampRef {
74
- current: FileStamp;
75
- }
76
- /** A stamp no real file can match, so the first load always reads the disk. */
77
- export declare function createFileStampRef(): FileStampRef;
78
- export declare function stampOf(stat: Stats): FileStamp;
79
- /** The accounts file lives next to the DSH home the launcher exports. */
68
+ /** The accounts database lives next to the DSH home the launcher exports. */
80
69
  export declare function defaultAccountsFilePath(): string;
81
70
  /**
82
- * Read the accounts file, or create it on first run with a fresh token
83
- * secret. The stamp ref is left pointing at the version just read.
84
- */
85
- export declare function loadAccountsFile(filePath: string, stamp: FileStampRef): AccountsFile;
86
- /**
87
- * Persist the accounts file atomically (temp file + rename) and leave the
88
- * stamp ref pointing at the freshly written version.
71
+ * Where the pre-SQLite accounts file lived. It is imported on first use and
72
+ * then renamed aside, so a deployment upgrades without re-registering anyone.
89
73
  */
90
- export declare function persistAccountsFile(filePath: string, file: AccountsFile, stamp: FileStampRef): void;
74
+ export declare function defaultLegacyAccountsFilePath(): string;
91
75
  /**
92
- * The qa-accounts CLI runs in its own process against the same file. Re-read
93
- * it whenever it changed on disk, before every read or read-modify-write:
94
- * without this a running Host would keep honoring tokens the CLI revoked
95
- * and its next save would resurrect the CLI's change from a stale snapshot.
96
- * One file, one writer at a time remains the deployment's discipline; LAN
97
- * scale keeps the stat per operation trivial.
98
- *
99
- * Returns the freshly loaded file, or undefined when the caller should keep
100
- * serving its in-memory state.
76
+ * A stable digest of one capability policy, so two chats admitted under the
77
+ * same policy share a stored snapshot. Keys are sorted because object key order
78
+ * is an artefact of construction, not part of the policy.
101
79
  */
102
- export declare function reloadAccountsFileIfChanged(filePath: string, stamp: FileStampRef): AccountsFile | undefined;
80
+ export declare function snapshotDigest(value: unknown): string;
103
81
  //# sourceMappingURL=file.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { QaAccountIdentityField, QaAccountProfile, QaAccountProfileInput, QaAccountRole, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaAccessUser, QaAdminAccountRow, QaClaimResult, QaEffectiveCapabilityPolicy, QaOwnershipEntry, QaSkillActivationRecord, QaUserAccess, QaWhoamiResult } from "../types.js";
2
- import type { StoredOwnership, StoredUser } from "./file.js";
2
+ import { type StoredOwnership, type StoredUser } from "./file.js";
3
3
  export { QaAccountsError } from "./errors.js";
4
4
  export type { QaAccountsErrorReason } from "./errors.js";
5
5
  export { defaultAccountsFilePath } from "./file.js";
@@ -15,18 +15,45 @@ export interface QaAccountsOptions {
15
15
  * omitting the list (the operator CLI) checks the key shape alone.
16
16
  */
17
17
  readonly identityFields?: readonly QaAccountIdentityField[];
18
+ /**
19
+ * Where a pre-SQLite accounts file may still live. Defaults to the `.json`
20
+ * sibling of the database; pass an explicit path when a deployment kept it
21
+ * somewhere else.
22
+ */
23
+ readonly legacyFilePath?: string;
24
+ }
25
+ /**
26
+ * What the caller knows about the live session it is gating. The store is
27
+ * file-backed and session-agnostic; the Host supplies these facts so the
28
+ * first-come auto-claim can refuse what it must not attach silently. Absent
29
+ * facts (the operator CLI, a session the Host has not materialized) keep the
30
+ * historical unbounded claim.
31
+ */
32
+ export interface QaSessionFacts {
33
+ /** Epoch ms the Host recorded as the session's creation. */
34
+ readonly createdAt?: number;
35
+ /** True when the session is a delegated child of another chat. */
36
+ readonly hasParent?: boolean;
18
37
  }
19
38
  /**
20
- * Host authority for QA accounts: the file-backed user list, the session
21
- * ownership map and the HMAC account tokens. All mutations are synchronous
22
- * and persisted atomically (temp file + rename); LAN scale keeps this trivial.
39
+ * How long an unowned session stays claimable at access time. Mirrors the
40
+ * admission bootstrap window: a session older than this has either user
41
+ * history or a settled composition, so silently attaching it to whoever
42
+ * asked first would hand over a conversation nobody assigned to them.
43
+ * Owners are unaffected — their claim already exists in the file.
44
+ */
45
+ export declare const QA_SESSION_CLAIM_WINDOW_MS = 120000;
46
+ /**
47
+ * Host authority for QA accounts: the account list, the session ownership map
48
+ * and the HMAC account tokens. Mutations are synchronous and land as the rows
49
+ * they change, in one transaction each.
23
50
  *
24
- * The class is the facade: token mechanics live in token.ts, the file format
25
- * and its atomic persistence plus the external-change probe in file.ts, and
26
- * the credential rules shared by registration and the operator CLI in
27
- * credentials.ts.
51
+ * The class is the facade: token mechanics live in token.ts, the tables and the
52
+ * legacy import in database.ts, and the credential rules shared by registration
53
+ * and the operator CLI in credentials.ts.
28
54
  */
29
55
  export declare class QaAccounts {
56
+ /** The database this store reads and writes. */
30
57
  readonly filePath: string;
31
58
  private file;
32
59
  private readonly authAttempts;
@@ -35,18 +62,26 @@ export declare class QaAccounts {
35
62
  private readonly allowRegistration;
36
63
  /** Character cap on a stored profile's agent guidance. */
37
64
  private readonly instructionsMaxLength;
38
- /** Declared handle fields; undefined lets an operator write any shape-valid key. */
39
- private readonly identityFields;
40
- /** mtime+size of the file as of the last load; the external-change probe. */
41
- private readonly fileStamp;
42
- constructor(filePath: string | undefined, options: QaAccountsOptions);
65
+ /** Whether a deployment declares which identity handles an account may hold. */
66
+ readonly identityFields: readonly QaAccountIdentityField[] | undefined;
67
+ /**
68
+ * The store's tables. Every mutation writes the rows it changes and the
69
+ * in-memory model is refreshed only when another process wrote — which is
70
+ * what keeps the cost of opening a chat independent of the deployment's age.
71
+ */
72
+ private readonly database;
73
+ constructor(configuredPath: string | undefined, options: QaAccountsOptions);
74
+ /** Close the underlying database; the store is unusable afterwards. */
75
+ close(): void;
43
76
  /**
44
- * Re-read the accounts file when it changed on disk, before every read or
45
- * read-modify-write. The probe itself and its cross-process rationale live
46
- * with the file layer (reloadAccountsFileIfChanged).
77
+ * Re-read the model when another process wrote the database, before every
78
+ * read or read-modify-write. The `qa-accounts` CLI runs in its own process
79
+ * against the same file, so without this a running Host would keep honoring
80
+ * tokens the CLI revoked. SQLite bumps `PRAGMA data_version` exactly when a
81
+ * *different* connection commits, which is the probe the previous
82
+ * mtime-and-size stamp of the JSON file used to provide.
47
83
  */
48
84
  private reloadIfChanged;
49
- private save;
50
85
  private pruneAuthAttempts;
51
86
  private assertAuthBudget;
52
87
  private userByEmail;
@@ -64,8 +99,15 @@ export declare class QaAccounts {
64
99
  * Ownership gate for one session: claim unowned sessions for the requesting
65
100
  * user (first come, first served — the pre-accounts migration path), refuse
66
101
  * foreign sessions unless the caller administers the deployment.
102
+ *
103
+ * The auto-claim is bounded when the caller can describe the live session
104
+ * ({@link QaSessionFacts}): a delegated subagent session is never claimed,
105
+ * and a session past the freshness window is refused instead of silently
106
+ * attached to whoever opened it first. Both refusals keep the file intact —
107
+ * the session stays unowned, so the operator CLI or a deliberate future
108
+ * claim can still resolve it.
67
109
  */
68
- ensureSessionAccess(token: string, sessionId: string): QaAccountUserPublic;
110
+ ensureSessionAccess(token: string, sessionId: string, facts?: QaSessionFacts): QaAccountUserPublic;
69
111
  /**
70
112
  * Atomically reserve a Host-generated session id for the token user before
71
113
  * the Session exists. Browser callers therefore never get a race window in
@@ -81,6 +123,36 @@ export declare class QaAccounts {
81
123
  claimSessions(token: string, sessionIds: readonly string[]): QaClaimResult;
82
124
  /** Session ids owned by the token's user, most recently claimed first. */
83
125
  ownedSessionIds(token: string): readonly string[];
126
+ /**
127
+ * Drop ownership records of sessions that are not chats at all.
128
+ *
129
+ * A delegated subagent session is an implementation detail of one answer:
130
+ * nothing legitimately owns one, so no grace period applies and every named
131
+ * id loses its record. Only ids the caller positively identified are
132
+ * touched — a record is an auth boundary, and an id that merely looks odd
133
+ * keeps its owner. What this reclaims is residue: records written before the
134
+ * claim and attestation paths learned to refuse a delegated child, each one
135
+ * keeping a subagent's session in some account's chat list.
136
+ *
137
+ * @param sessionIds - the ids the caller identified as delegated children.
138
+ * @returns the session ids whose records were dropped.
139
+ */
140
+ pruneDelegatedOwnership(sessionIds: ReadonlySet<string>): readonly string[];
141
+ /**
142
+ * Drop ownership records for chats the Harness no longer knows.
143
+ *
144
+ * An ownership record is a chat's auth boundary — `ensureSessionAccess`
145
+ * claims an unowned session for whoever asks first — so a record is removed
146
+ * only when the caller reports the session gone *and* the claim is older
147
+ * than `graceHours`. A reservation racing its own creation therefore keeps
148
+ * its owner, and a live chat never becomes claimable by someone else. What
149
+ * this reclaims is the residue of deleted chats, which nothing else removes.
150
+ *
151
+ * @param isLive - whether the Harness still knows this session.
152
+ * @param graceHours - youngest claim the sweep may touch.
153
+ * @returns the session ids whose records were dropped.
154
+ */
155
+ pruneVanishedSessions(isLive: (sessionId: string) => boolean, graceHours: number): readonly string[];
84
156
  /**
85
157
  * Every ownership entry with the owner's display name resolved at read
86
158
  * time, oldest claim first. Admin-only: this is the cross-user view the
@@ -131,7 +203,14 @@ export declare class QaAccounts {
131
203
  readonly adminPreview?: boolean;
132
204
  readonly capabilitySnapshot?: QaEffectiveCapabilityPolicy;
133
205
  }[];
134
- /** Replace one account's authorization role, addressed by stable id. */
206
+ /**
207
+ * Replace one account's authorization role, addressed by stable id.
208
+ *
209
+ * The union is re-checked here even though the type names it: the value
210
+ * arrives from the browser's admin console over the wire, and a role outside
211
+ * the union would be stored as-is and then crash every later permission
212
+ * check (`PERMISSIONS[role]` lookup) for that account.
213
+ */
135
214
  setAccessRole(userId: string, role: QaAccountRole): QaAccountUserPublic;
136
215
  /**
137
216
  * Disable or enable one account by id. Same semantics as the operator
@@ -20,6 +20,8 @@ export interface QaManualQueueEntry {
20
20
  readonly createdAt: string;
21
21
  }
22
22
  export declare function defaultQualityFilePath(): string;
23
+ /** The pre-SQLite quality file, imported once on first use. */
24
+ export declare function defaultLegacyQualityFilePath(): string;
23
25
  /**
24
26
  * Atomic, externally reloadable store for the quality layer. Writes are
25
27
  * synchronous and persisted with a temp file plus rename, matching the accounts
@@ -33,9 +35,11 @@ export declare function defaultQualityFilePath(): string;
33
35
  */
34
36
  export declare class QaQualityStore {
35
37
  readonly filePath: string;
38
+ private readonly storage;
36
39
  private file;
37
- private fileStamp;
40
+ private observedDataVersion;
38
41
  constructor(filePath?: string);
42
+ close(): void;
39
43
  /**
40
44
  * Record or replace one user's rating of one message. Re-rating the same
41
45
  * message overwrites in place rather than appending: a reviewer reads the
@@ -75,8 +79,22 @@ export declare class QaQualityStore {
75
79
  auditEvents(): readonly QaAdminAuditEvent[];
76
80
  /** Serialize one before/after value, refusing what no reader could use. */
77
81
  private snapshot;
82
+ /** Write one record, moving it to the end of its family's order. */
83
+ private upsertRow;
84
+ /** Append a record whose identity the store mints itself. */
85
+ private appendRow;
86
+ private deleteRow;
87
+ private applyCap;
88
+ private rowsOf;
89
+ private readDataVersion;
78
90
  private reload;
79
91
  private load;
80
- private persist;
92
+ /**
93
+ * Import a pre-SQLite `qa-quality.json` exactly once, then rename it aside.
94
+ * A row the database already holds wins, so a leftover file cannot overwrite
95
+ * a verdict a person has since given.
96
+ */
97
+ private importLegacyFile;
98
+ private assertImportArrived;
81
99
  }
82
100
  //# sourceMappingURL=quality-store.d.ts.map
@@ -40,4 +40,4 @@ export declare function formatRelative(iso: string, now?: number): string;
40
40
  /** Milliseconds as a short duration; tool timings are second-scale. */
41
41
  export declare function formatDuration(ms: number | undefined): string;
42
42
  export declare function formatCount(value: number): string;
43
- //# sourceMappingURL=format.d.ts.map
43
+ //# sourceMappingURL=copy.d.ts.map
@@ -1,13 +1,12 @@
1
1
  import type { QaSource } from "../../types.js";
2
- import { type QaSourceRefs } from "./source-refs.js";
3
- /** Inline source awareness handed down from the owning assistant message. */
4
- export interface MarkdownSourceContext {
5
- readonly refs?: QaSourceRefs;
6
- /** Open a path-backed source in the sources drawer's detail view. */
7
- readonly onSourceOpen?: (source: QaSource) => void;
8
- }
2
+ import type { QaSourceRefs } from "./source-refs.js";
3
+ export type { MarkdownSourceContext } from "../markdown/source-chip.js";
9
4
  /**
10
- * Deliberately small, HTML-free Markdown renderer for assistant-visible text.
5
+ * Assistant-visible Markdown. The grammar lives in `../markdown/`: blocks are
6
+ * parsed once per text value, inline content is resolved per block, and a link
7
+ * or inline-code token that names a source the owning message knows about
8
+ * renders as a source chip instead of plain text.
9
+ *
11
10
  * Memoized on the text value: stream updates re-render only the message whose
12
11
  * text actually changed, so committed history is never re-parsed.
13
12
  */
@@ -14,7 +14,9 @@ export interface QaChatRow {
14
14
  * Project this browser's indexed chat ids onto the host session list, most
15
15
  * recently updated first. Opening a chat is not an update: only the host's
16
16
  * `updatedAt` (fresh messages) orders the list. Ids the host no longer lists
17
- * are skipped (the controller prunes them on switch).
17
+ * are skipped (the controller prunes them on switch), and so is a delegated
18
+ * child: a subagent's session is not a chat, even when an ownership record
19
+ * from an older release still names it.
18
20
  */
19
21
  export declare function buildChatRows(chatIds: readonly string[], byId: Readonly<Record<string, SessionSummary>>, activeId: string | null, ownerNameOf?: (sessionId: string) => string | undefined, now?: number): readonly QaChatRow[];
20
22
  /** One per-owner section of the admin sidebar grouping. */
@@ -1,8 +1,6 @@
1
1
  import type { QaSource, QaTurnSources, ResolvedQaSurfaceConfig } from "../../types.js";
2
2
  import type { QaBoundSourceApi } from "../types.js";
3
- export declare function SourceIcon({ kind }: {
4
- readonly kind: QaSource["kind"];
5
- }): import("react/jsx-runtime").JSX.Element;
3
+ export { SourceIcon } from "./source-icon.js";
6
4
  export interface QaSourcesPanelProps {
7
5
  readonly sources: readonly QaSource[];
8
6
  readonly complete: boolean;
@@ -0,0 +1,12 @@
1
+ import type { QaSource } from "../../types.js";
2
+ /**
3
+ * Leaf source-kind vocabulary: the icon and the group labels a kind wears.
4
+ * Kept free of component imports so the markdown renderer's chips can share
5
+ * it without pulling the sources panel (and its markdown dependency) into
6
+ * every inline parse.
7
+ */
8
+ export declare const KIND_LABELS: Readonly<Record<QaSource["kind"], string>>;
9
+ export declare function SourceIcon({ kind }: {
10
+ readonly kind: QaSource["kind"];
11
+ }): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=source-icon.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A per-second `Date.now()` that runs only while `active` (and re-syncs the
3
+ * instant it turns on). The thinking phrase and the running work group read
4
+ * the same clock through this hook, so the surface never stacks duplicate
5
+ * second intervals for one turn.
6
+ */
7
+ export declare function useNow(active: boolean): number;
8
+ //# sourceMappingURL=use-now.d.ts.map
@@ -0,0 +1,15 @@
1
+ import type { SessionSummary } from "@deepseek-ai/dsh-api-session-controller/client";
2
+ /**
3
+ * Whether one session-list entry is a delegated child — a subagent's session —
4
+ * rather than a chat.
5
+ *
6
+ * A delegated run is an implementation detail of a single answer: the Host
7
+ * refuses to attest it, its sources reach the parent chat through the
8
+ * provenance inheritance flow, and it has no owner. The host list the surface
9
+ * already follows carries both marks (`origin` and `parentId`), so every
10
+ * projection that talks about chats asks this one question instead of
11
+ * re-deriving it — and a chat row, a chat count and a claim batch can never
12
+ * disagree about what a chat is.
13
+ */
14
+ export declare function isDelegatedSession(summary: Pick<SessionSummary, "origin" | "parentId"> | undefined): boolean;
15
+ //# sourceMappingURL=lineage.d.ts.map
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * One fenced code block: the language banner, a copy button, and the code
4
+ * itself. Highlighting is our own scanner (see `highlight.ts`) — the fence is
5
+ * memoized on its source and info string, so a streaming append re-tokenizes
6
+ * only the fence that grew.
7
+ */
8
+ export declare const CodeBlock: import("react").MemoExoticComponent<({ code, lang, }: {
9
+ readonly code: string;
10
+ readonly lang?: string;
11
+ }) => import("react/jsx-runtime").JSX.Element>;
12
+ /** The globe an external link wears, drawn like the surface's other icons. */
13
+ export declare function LinkGlyph(): ReactNode;
14
+ //# sourceMappingURL=CodeBlock.d.ts.map