@timurproko/a1 0.1.8-dev.290 → 0.1.8-dev.322

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 (112) hide show
  1. package/README.md +21 -0
  2. package/bin/guardian.js +4 -3
  3. package/bin/ui.js +4 -2
  4. package/bin/update-recovery.js +3 -1
  5. package/dist/composition/owned-ui.js +13 -1
  6. package/dist/contracts/owned-ui/index.d.ts +1 -0
  7. package/dist/contracts/owned-ui/index.js +1 -0
  8. package/dist/contracts/owned-ui/prompt-history.d.ts +37 -0
  9. package/dist/contracts/owned-ui/prompt-history.js +23 -0
  10. package/dist/features/launch/profile-paths.d.ts +2 -0
  11. package/dist/features/launch/profile-paths.js +11 -0
  12. package/dist/features/prompt-history/index.d.ts +2 -0
  13. package/dist/features/prompt-history/index.js +2 -0
  14. package/dist/features/prompt-history/paths.d.ts +4 -0
  15. package/dist/features/prompt-history/paths.js +16 -0
  16. package/dist/features/prompt-history/service.d.ts +24 -0
  17. package/dist/features/prompt-history/service.js +260 -0
  18. package/dist/features/prompt-history/store.d.ts +15 -0
  19. package/dist/features/prompt-history/store.js +166 -0
  20. package/dist/features/prompt-history/worker.d.ts +9 -0
  21. package/dist/features/prompt-history/worker.js +42 -0
  22. package/dist/foundation/launch-context/index.d.ts +29 -0
  23. package/dist/foundation/launch-context/index.js +83 -0
  24. package/dist/foundation/launch-guardian/main.js +4 -3
  25. package/dist/foundation/release/bootstrap.d.ts +1 -1
  26. package/dist/foundation/release/bootstrap.js +18 -12
  27. package/dist/foundation/release/cohort-state.d.ts +1 -0
  28. package/dist/foundation/release/cohort-state.js +4 -0
  29. package/dist/foundation/release/release-gc.js +2 -2
  30. package/dist/foundation/release/release-store.js +10 -3
  31. package/dist/foundation/release/release.d.ts +3 -1
  32. package/dist/foundation/release/release.js +7 -3
  33. package/dist/foundation/release/restart-certification.d.ts +1 -0
  34. package/dist/foundation/release/restart-certification.js +5 -0
  35. package/dist/foundation/release/update-recovery.d.ts +2 -0
  36. package/dist/foundation/release/update-recovery.js +4 -0
  37. package/dist/foundation/release/update.js +5 -0
  38. package/dist/foundation/release/warmup.d.ts +1 -1
  39. package/dist/foundation/release/warmup.js +5 -4
  40. package/dist/foundation/startup/startup-runtime.js +13 -11
  41. package/dist/foundation/supervision/main.js +8 -7
  42. package/dist/integrations/pi/components/editor-interaction.d.ts +38 -0
  43. package/dist/integrations/pi/components/editor-interaction.js +1 -0
  44. package/dist/integrations/pi/components/history-editor-loader.d.ts +5 -0
  45. package/dist/integrations/pi/components/history-editor-loader.js +5 -0
  46. package/dist/integrations/pi/components/index.d.ts +1 -0
  47. package/dist/integrations/pi/components/index.js +1 -0
  48. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  49. package/dist/integrations/pi/components/owned-editor-ux.js +95 -22
  50. package/dist/integrations/pi/components/shell-editor-autocomplete.js +12 -1
  51. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  52. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  53. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  54. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  55. package/dist/integrations/pi/components/shell-shared-facade.d.ts +15 -2
  56. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  57. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +13 -9
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +137 -133
  60. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +280 -0
  61. package/dist/integrations/pi/components/upstream/history/editor-core.js +2062 -0
  62. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +32 -0
  63. package/dist/integrations/pi/components/upstream/history/kill-ring.js +48 -0
  64. package/dist/integrations/pi/components/upstream/history/printable-key.d.ts +1 -0
  65. package/dist/integrations/pi/components/upstream/history/printable-key.js +40 -0
  66. package/dist/integrations/pi/components/upstream/history/text-helpers.d.ts +14 -0
  67. package/dist/integrations/pi/components/upstream/history/text-helpers.js +28 -0
  68. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +22 -0
  69. package/dist/integrations/pi/components/upstream/history/undo-stack.js +36 -0
  70. package/dist/integrations/pi/components/upstream/history/word-navigation.d.ts +24 -0
  71. package/dist/integrations/pi/components/upstream/history/word-navigation.js +100 -0
  72. package/dist/integrations/pi/engine/adapter.d.ts +13 -1
  73. package/dist/integrations/pi/engine/adapter.js +86 -25
  74. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  75. package/dist/integrations/pi/engine/changelog.js +63 -0
  76. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  77. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  78. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  79. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +4 -1
  80. package/dist/integrations/pi/session-ui/prompt-chips.js +40 -13
  81. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +30 -0
  82. package/dist/integrations/pi/session-ui/prompt-history-controller.js +147 -0
  83. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +33 -0
  84. package/dist/integrations/pi/session-ui/session-shell-root.js +68 -74
  85. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  86. package/dist/integrations/pi/session-ui/session-shell.js +98 -12
  87. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  88. package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
  89. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  90. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
  91. package/dist/native/darwin-arm64/manifest.json +1 -1
  92. package/dist/native/linux-x64/manifest.json +1 -1
  93. package/dist/native/win32-x64/manifest.json +2 -2
  94. package/dist/native/win32-x64/process-guardian.exe +0 -0
  95. package/dist/product-identity.d.ts +2 -2
  96. package/dist/product-identity.js +3 -36
  97. package/dist/product-identity.json +1 -22
  98. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  99. package/dist/ui/components/transcript-viewport.js +23 -2
  100. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  101. package/dist/ui/components/visible-hyperlinks.js +1 -0
  102. package/dist/ui/settings/declarations.js +19 -0
  103. package/dist/ui/settings/sections.js +11 -1
  104. package/dist/ui/settings/session.js +13 -4
  105. package/docs/architecture/boundaries.md +2 -0
  106. package/docs/architecture/history-editor-provenance.md +96 -0
  107. package/docs/architecture/internal-naming.md +89 -0
  108. package/docs/architecture/project-structure.md +1 -0
  109. package/docs/architecture/resource-and-data-policy.md +27 -1
  110. package/docs/architecture/toolchain.md +15 -1
  111. package/docs/features/prompt-history.md +149 -0
  112. package/package.json +8 -3
package/README.md CHANGED
@@ -45,6 +45,27 @@ a1 pi # vanilla Pi oracle: ~/.pi/agent
45
45
  Unsupported commands exit quietly without launching anything. The removed
46
46
  `update:<preview>` forms are not aliases; use `update --develop`.
47
47
 
48
+ ## Prompt suggestions
49
+
50
+ Bare A1's `/settings` → Agent → Prompt suggestions controls faint next-prompt
51
+ predictions. It is enabled by default and uses one additional background request
52
+ with the selected model for eligible turns. Disabling it takes effect immediately.
53
+ Tab accepts a suggestion as editable text; Enter submits it afterward. The setting
54
+ remains stored in A1's profile, and `a1 pi` is unchanged.
55
+
56
+ ## Prompt history
57
+
58
+ Bare A1 recalls recent unique prompts across sessions with Up/Down. History is
59
+ stored in a separate profile-local SQLite database at
60
+ `~/.a1/data/history/<profile-id>.sqlite3`, or under `<A1_DATA_DIR>/history` when
61
+ explicitly overridden. Other application data stays in its existing location.
62
+ `/settings` → History controls persistence and the 10-100-entry limit (default
63
+ 100), effective on next start. The new default starts fresh: old platform history
64
+ is neither imported nor automatically deleted. Ordinary upgrades and npm
65
+ uninstall/reinstall preserve saved history. History is unencrypted user text;
66
+ images are not reattached. See
67
+ [prompt history](docs/features/prompt-history.md) for privacy, storage, and removal.
68
+
48
69
  ## Extensions
49
70
 
50
71
  Pi extension packages install into A1's own profile (`~/.a1/agent`), so bare `a1`
package/bin/guardian.js CHANGED
@@ -3,13 +3,14 @@
3
3
  const startup = await import("../dist/foundation/startup/index.js");
4
4
  startup.enableEnvironmentCompileCache(process.env);
5
5
  await startup.markStartupPhase(process.env, "guardian-start");
6
- const { fileURLToPath } = await import("node:url");
7
6
  const { resolve } = await import("node:path");
8
7
  const { parseSessionSelection } = await import("../dist/foundation/lifecycle/index.js");
9
8
  const { runLaunchGuardian } = await import("../dist/foundation/launch-guardian/index.js");
10
9
 
11
- const releaseRoot = process.env.A1_RELEASE_ROOT ?? fileURLToPath(new URL("..", import.meta.url));
12
- const profileId = process.env.A1_LAUNCH_PROFILE ?? "a1";
10
+ const { readLaunchContext } = await import("../dist/foundation/launch-context/index.js");
11
+ const launchContext = readLaunchContext(process.env, "release");
12
+ const releaseRoot = launchContext.releaseRoot;
13
+ const profileId = readLaunchContext(process.env, "profile").launchProfile;
13
14
 
14
15
  Promise.resolve().then(() => runLaunchGuardian({
15
16
  sessionSelection: parseSessionSelection(process.argv.slice(2)),
package/bin/ui.js CHANGED
@@ -2,14 +2,16 @@
2
2
 
3
3
  const startup = await import("../dist/foundation/startup/index.js");
4
4
  startup.enableEnvironmentCompileCache(process.env);
5
- const profile = process.env.A1_LAUNCH_PROFILE ?? "a1";
5
+ const { readLaunchContext } = await import("../dist/foundation/launch-context/index.js");
6
+ const launchContext = readLaunchContext(process.env, "profile");
7
+ const profile = launchContext.launchProfile;
6
8
  const { installFatalExit } = await import("../dist/foundation/terminal-cleanup/index.js");
7
9
  const { resolveProductPaths } = await import("../dist/foundation/lifecycle/index.js");
8
10
  const { join } = await import("node:path");
9
11
  let runningApplication;
10
12
  const fatal = profile === "a1" ? installFatalExit({
11
13
  directory: join(resolveProductPaths().runtimeDir, "crashes"),
12
- releaseId: process.env.A1_RELEASE_ID,
14
+ releaseId: launchContext.releaseId,
13
15
  dispose: () => runningApplication?.dispose(),
14
16
  }) : undefined;
15
17
  // Performance: begin the exact launch graph together while the trace write is pending.
@@ -140,6 +140,7 @@ async function runRecoveredCommand(value, arguments_) {
140
140
 
141
141
  async function readCapsule(path) {
142
142
  const value = JSON.parse(await readFile(path, "utf8"));
143
+ if (value.launchContract !== "neutral-launch-v1") throw new Error("Unsupported private recovery contract; stop existing processes and install directly with npm. Preserve settings, sessions, and history.");
143
144
  if (value.schema !== SCHEMA || typeof value.transactionId !== "string" || typeof value.packageName !== "string"
144
145
  || typeof value.packageName !== "string" || !value.packageName.includes("/") || typeof value.targetVersion !== "string" || typeof value.packageRoot !== "string" || typeof value.globalRoot !== "string"
145
146
  || typeof value.launcherRoot !== "string" || !Array.isArray(value.launchers) || typeof value.priorReleaseId !== "string" || typeof value.priorReleaseRoot !== "string"
@@ -178,6 +179,7 @@ async function readCapsule(path) {
178
179
  assertDirectChild(releasesRoot, priorReleaseRoot);
179
180
  if (priorReleaseRoot.split(sep).at(-1) !== value.priorReleaseId) throw new Error("A1 update recovery prior release identity is invalid");
180
181
  const priorManifest = JSON.parse(await readFile(resolve(priorReleaseRoot, value.releaseManifestName), "utf8"));
182
+ if (priorManifest.launchContract !== "neutral-launch-v1") throw new Error("Unsupported prior private launch contract; manual cutover is required.");
181
183
  if (priorManifest.releaseId !== value.priorReleaseId || priorManifest.contentDigest !== value.priorContentDigest) {
182
184
  throw new Error("A1 update recovery prior release manifest is invalid");
183
185
  }
@@ -190,7 +192,7 @@ async function installedTargetEntry(value) {
190
192
  try {
191
193
  const manifest = JSON.parse(await readFile(resolve(value.packageRoot, "package.json"), "utf8"));
192
194
  const entry = resolve(value.packageRoot, "bin", "cli.js");
193
- if (manifest.name !== value.packageName || manifest.version !== value.targetVersion || !(await lstat(entry)).isFile()) return null;
195
+ if (manifest.privateLaunchContract !== "neutral-launch-v1" || manifest.name !== value.packageName || manifest.version !== value.targetVersion || !(await lstat(entry)).isFile()) return null;
194
196
  return entry;
195
197
  } catch { return null; }
196
198
  }
@@ -1,5 +1,7 @@
1
+ import { PromptHistoryService } from "../features/prompt-history/index.js";
2
+ import { resolvePromptHistoryDataDir } from "../features/launch/index.js";
1
3
  import { resolveProductPaths } from "../foundation/lifecycle/index.js";
2
- import { applyConfiguredPiTheme, getAvailablePiThemes } from "../integrations/pi/components/index.js";
4
+ import { applyConfiguredPiTheme, getAvailablePiThemes, loadHistoryEditor } from "../integrations/pi/components/index.js";
3
5
  import { createPiEngineAdapter, } from "../integrations/pi/engine/index.js";
4
6
  import { OwnedUiSessionShell } from "../integrations/pi/session-ui/index.js";
5
7
  import { OwnedUiSettingsSession, OwnedUiSettingsStore } from "../ui/settings/index.js";
@@ -42,6 +44,15 @@ export async function composeOwnedUi(options = {}) {
42
44
  enabled: () => settings.value("promptSuggestions") !== false,
43
45
  onChange: (listener) => settings.onChange(() => listener(settings.value("promptSuggestions") !== false)),
44
46
  };
47
+ const historyLimit = settings?.value("promptHistoryMaxItems");
48
+ const promptHistory = settings === null || !ownedSurfaces || settings.value("promptHistoryEnabled") === false ? null : {
49
+ limit: typeof historyLimit === "number" ? historyLimit : 100,
50
+ store: new PromptHistoryService({
51
+ dataDir: resolvePromptHistoryDataDir(),
52
+ profileRoot: adapter.agentDir,
53
+ limit: typeof historyLimit === "number" ? historyLimit : 100,
54
+ }),
55
+ };
45
56
  const shell = new OwnedUiSessionShell({
46
57
  backend: adapter,
47
58
  cwd: adapter.cwd,
@@ -50,6 +61,7 @@ export async function composeOwnedUi(options = {}) {
50
61
  ...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
51
62
  ...(viewportSettings === null ? {} : { viewportSettings }),
52
63
  ...(promptSuggestions === null ? {} : { promptSuggestions }),
64
+ ...(promptHistory === null ? {} : { promptHistory: { ...promptHistory, editor: await loadHistoryEditor() } }),
53
65
  });
54
66
  const application = {
55
67
  get disposed() { return adapter.disposed; },
@@ -2,4 +2,5 @@ export * from "./extension-ui.js";
2
2
  export * from "./model.js";
3
3
  export * from "./image-attachments.js";
4
4
  export * from "./prompt-suggestions.js";
5
+ export * from "./prompt-history.js";
5
6
  export * from "./validation.js";
@@ -2,4 +2,5 @@ export * from "./extension-ui.js";
2
2
  export * from "./model.js";
3
3
  export * from "./image-attachments.js";
4
4
  export * from "./prompt-suggestions.js";
5
+ export * from "./prompt-history.js";
5
6
  export * from "./validation.js";
@@ -0,0 +1,37 @@
1
+ export declare const PROMPT_HISTORY_EDITOR_REPLACEMENT: Readonly<{
2
+ readonly id: "persistent-prompt-history";
3
+ readonly kind: "replacement";
4
+ readonly slot: "editor";
5
+ readonly replaces: readonly string[];
6
+ }>;
7
+ export type PromptHistoryKind = "prompt" | "steer" | "follow-up" | "bash" | "slash";
8
+ export interface PromptHistorySubmission {
9
+ readonly id: string;
10
+ readonly text: string;
11
+ readonly timestamp: number;
12
+ readonly kind: PromptHistoryKind;
13
+ readonly cwd?: string;
14
+ readonly sessionId?: string;
15
+ }
16
+ export interface PromptHistorySnapshot {
17
+ readonly revision: number;
18
+ readonly limit: number;
19
+ readonly entries: readonly {
20
+ readonly text: string;
21
+ readonly submissionId: string;
22
+ }[];
23
+ }
24
+ export type PromptHistoryFailure = "unavailable" | "busy" | "capacity" | "oversized" | "schema" | "corrupt" | "shutdown";
25
+ export type PromptHistoryResult = "committed" | "skipped";
26
+ export interface PromptHistoryPort {
27
+ start(): void;
28
+ record(submission: PromptHistorySubmission): Promise<PromptHistoryResult>;
29
+ refresh(): void;
30
+ onSnapshot(listener: (snapshot: PromptHistorySnapshot) => void): () => void;
31
+ onFailure(listener: (code: PromptHistoryFailure) => void): () => void;
32
+ close(): Promise<void>;
33
+ }
34
+ export declare const PROMPT_HISTORY_MAX_ENTRY_BYTES: number;
35
+ export declare const PROMPT_HISTORY_MAX_TEXT_BYTES: number;
36
+ export declare const PROMPT_HISTORY_MAX_PENDING = 32;
37
+ export declare function assertPromptHistorySubmission(value: unknown): asserts value is PromptHistorySubmission;
@@ -0,0 +1,23 @@
1
+ export const PROMPT_HISTORY_EDITOR_REPLACEMENT = Object.freeze({
2
+ id: "persistent-prompt-history",
3
+ kind: "replacement",
4
+ slot: "editor",
5
+ replaces: Object.freeze(["current-session-history-source", "adjacent-duplicate-policy", "recall-caret-placement", "history-border"]),
6
+ });
7
+ export const PROMPT_HISTORY_MAX_ENTRY_BYTES = 1024 * 1024;
8
+ export const PROMPT_HISTORY_MAX_TEXT_BYTES = 8 * 1024 * 1024;
9
+ export const PROMPT_HISTORY_MAX_PENDING = 32;
10
+ export function assertPromptHistorySubmission(value) {
11
+ if (typeof value !== "object" || value === null || Array.isArray(value))
12
+ throw new TypeError("Invalid history submission");
13
+ const item = value;
14
+ const allowed = new Set(["id", "text", "timestamp", "kind", "cwd", "sessionId"]);
15
+ if (Object.keys(item).some(key => !allowed.has(key))
16
+ || typeof item.id !== "string" || !/^[a-zA-Z0-9-]{1,128}$/.test(item.id)
17
+ || typeof item.text !== "string" || !item.text.trim() || item.text !== item.text.trim()
18
+ || typeof item.timestamp !== "number" || !Number.isSafeInteger(item.timestamp) || item.timestamp < 0
19
+ || typeof item.kind !== "string" || !["prompt", "steer", "follow-up", "bash", "slash"].includes(item.kind)
20
+ || item.cwd !== undefined && typeof item.cwd !== "string"
21
+ || item.sessionId !== undefined && typeof item.sessionId !== "string")
22
+ throw new TypeError("Invalid history submission");
23
+ }
@@ -11,4 +11,6 @@ export interface LaunchProfilePathOptions {
11
11
  readonly platform?: NodeJS.Platform;
12
12
  }
13
13
  export declare function resolveLaunchProfilePaths(options?: LaunchProfilePathOptions): LaunchProfilePaths;
14
+ /** Select history storage without relocating control state or probing former stores. */
15
+ export declare function resolvePromptHistoryDataDir(options?: LaunchProfilePathOptions): string;
14
16
  export declare function configurationRootForProfile(profileId: LaunchProfileId, paths: LaunchProfilePaths): string | null;
@@ -1,6 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { posix, win32 } from "node:path";
3
3
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
+ import { resolveProductPaths } from "../../foundation/lifecycle/index.js";
4
5
  export function resolveLaunchProfilePaths(options = {}) {
5
6
  const environment = options.environment ?? process.env;
6
7
  const path = options.platform === "win32" || (options.platform === undefined && process.platform === "win32") ? win32 : posix;
@@ -13,6 +14,16 @@ export function resolveLaunchProfilePaths(options = {}) {
13
14
  agentProfile,
14
15
  });
15
16
  }
17
+ /** Select history storage without relocating control state or probing former stores. */
18
+ export function resolvePromptHistoryDataDir(options = {}) {
19
+ const environment = options.environment ?? process.env;
20
+ const platform = options.platform ?? process.platform;
21
+ if (environment[PRODUCT_IDENTITY.environment.dataDir] !== undefined) {
22
+ return resolveProductPaths(environment, platform).dataDir;
23
+ }
24
+ const path = platform === "win32" ? win32 : posix;
25
+ return path.join(resolveLaunchProfilePaths(options).managedStateRoot, "data");
26
+ }
16
27
  export function configurationRootForProfile(profileId, paths) {
17
28
  return profileId === "a1" ? paths.agentProfile : null;
18
29
  }
@@ -0,0 +1,2 @@
1
+ export { PromptHistoryService } from "./service.js";
2
+ export { resolvePromptHistoryPath } from "./paths.js";
@@ -0,0 +1,2 @@
1
+ export { PromptHistoryService } from "./service.js";
2
+ export { resolvePromptHistoryPath } from "./paths.js";
@@ -0,0 +1,4 @@
1
+ export declare function resolvePromptHistoryPath(dataDir: string, profileRoot: string, platform?: NodeJS.Platform): {
2
+ path: string;
3
+ profileId: string;
4
+ };
@@ -0,0 +1,16 @@
1
+ import { createHash } from "node:crypto";
2
+ import { posix, win32 } from "node:path";
3
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
+ export function resolvePromptHistoryPath(dataDir, profileRoot, platform = process.platform) {
5
+ const paths = platform === "win32" ? win32 : posix;
6
+ if (!paths.isAbsolute(dataDir) || !paths.isAbsolute(profileRoot) || dataDir.includes("\0") || profileRoot.includes("\0"))
7
+ throw new TypeError("History requires absolute roots");
8
+ let normalized = paths.normalize(profileRoot);
9
+ if (normalized.length > paths.parse(normalized).root.length)
10
+ normalized = normalized.replace(/[\\/]+$/, "");
11
+ if (platform === "win32")
12
+ normalized = normalized.replaceAll("\\", "/").toLowerCase();
13
+ const digest = createHash("sha256").update(JSON.stringify([1, PRODUCT_IDENTITY.commandName, normalized])).digest("hex");
14
+ const profileId = `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}${digest}`;
15
+ return { path: paths.join(dataDir, "history", `${profileId}.sqlite3`), profileId };
16
+ }
@@ -0,0 +1,24 @@
1
+ import type { Worker } from "node:worker_threads";
2
+ import { type PromptHistoryFailure, type PromptHistoryPort, type PromptHistoryResult, type PromptHistorySnapshot, type PromptHistorySubmission } from "../../contracts/owned-ui/index.js";
3
+ /** Owns a finite asynchronous history queue; storage faults never throw into prompt dispatch. */
4
+ export declare class PromptHistoryService implements PromptHistoryPort {
5
+ #private;
6
+ readonly options: {
7
+ dataDir: string;
8
+ profileRoot: string;
9
+ limit: number;
10
+ createWorker?: () => Worker;
11
+ };
12
+ constructor(options: {
13
+ dataDir: string;
14
+ profileRoot: string;
15
+ limit: number;
16
+ createWorker?: () => Worker;
17
+ });
18
+ start(): void;
19
+ record(submission: PromptHistorySubmission): Promise<PromptHistoryResult>;
20
+ refresh(): void;
21
+ onSnapshot(listener: (snapshot: PromptHistorySnapshot) => void): () => void;
22
+ onFailure(listener: (code: PromptHistoryFailure) => void): () => void;
23
+ close(): Promise<void>;
24
+ }
@@ -0,0 +1,260 @@
1
+ import { assertPromptHistorySubmission, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_PENDING, PROMPT_HISTORY_MAX_TEXT_BYTES } from "../../contracts/owned-ui/index.js";
2
+ import { resolvePromptHistoryPath } from "./paths.js";
3
+ /** Owns a finite asynchronous history queue; storage faults never throw into prompt dispatch. */
4
+ export class PromptHistoryService {
5
+ options;
6
+ #snapshots = new Set();
7
+ #failures = new Set();
8
+ #reported = new Set();
9
+ #queue = [];
10
+ #worker;
11
+ #active;
12
+ #timer;
13
+ #poll;
14
+ #sequence = 0;
15
+ #bytes = 0;
16
+ #writes = 0;
17
+ #started = false;
18
+ #ready = false;
19
+ #failed = false;
20
+ #closing = false;
21
+ #reading = false;
22
+ #refreshPending = false;
23
+ #lastRevision = -1;
24
+ #closePromise;
25
+ constructor(options) {
26
+ this.options = options;
27
+ }
28
+ start() {
29
+ if (this.#started || this.#closing)
30
+ return;
31
+ this.#started = true;
32
+ setImmediate(() => { void this.#open().catch(() => this.#fail("unavailable")); });
33
+ }
34
+ record(submission) {
35
+ if (this.#failed || this.#closing)
36
+ return Promise.resolve("skipped");
37
+ try {
38
+ assertPromptHistorySubmission(submission);
39
+ }
40
+ catch {
41
+ this.#report("unavailable");
42
+ return Promise.resolve("skipped");
43
+ }
44
+ const bytes = Buffer.byteLength(submission.text);
45
+ if (bytes > PROMPT_HISTORY_MAX_ENTRY_BYTES) {
46
+ this.#report("oversized");
47
+ return Promise.resolve("skipped");
48
+ }
49
+ if (this.#writes >= PROMPT_HISTORY_MAX_PENDING || this.#bytes + bytes > PROMPT_HISTORY_MAX_TEXT_BYTES) {
50
+ this.#report("capacity");
51
+ return Promise.resolve("skipped");
52
+ }
53
+ const candidate = {
54
+ id: submission.id, text: submission.text, timestamp: submission.timestamp, kind: submission.kind,
55
+ ...(submission.cwd !== undefined && Buffer.byteLength(submission.cwd) <= 8192 ? { cwd: submission.cwd } : {}),
56
+ ...(submission.sessionId !== undefined && Buffer.byteLength(submission.sessionId) <= 256 ? { sessionId: submission.sessionId } : {}),
57
+ };
58
+ this.start();
59
+ this.#writes++;
60
+ this.#bytes += bytes;
61
+ return new Promise(resolve => {
62
+ this.#queue.push({ request: { id: ++this.#sequence, kind: "record", submission: candidate }, done: (_value, ok) => {
63
+ this.#writes--;
64
+ this.#bytes -= bytes;
65
+ resolve(ok ? "committed" : "skipped");
66
+ if (ok)
67
+ this.refresh();
68
+ } });
69
+ this.#drain();
70
+ });
71
+ }
72
+ refresh() {
73
+ if (this.#closing || this.#failed)
74
+ return;
75
+ this.start();
76
+ if (this.#reading) {
77
+ this.#refreshPending = true;
78
+ return;
79
+ }
80
+ this.#reading = true;
81
+ const observers = [...this.#snapshots];
82
+ this.#queue.push({ request: { id: ++this.#sequence, kind: "read" }, done: (value, ok) => {
83
+ this.#reading = false;
84
+ if (ok) {
85
+ if (!isSnapshot(value)) {
86
+ this.#fail("corrupt");
87
+ return;
88
+ }
89
+ if (value.revision !== this.#lastRevision && observers.some(listener => this.#snapshots.has(listener))) {
90
+ this.#lastRevision = value.revision;
91
+ for (const listener of observers) {
92
+ if (!this.#snapshots.has(listener))
93
+ continue;
94
+ try {
95
+ listener(value);
96
+ }
97
+ catch { /* Security: observer errors must not escape the worker event handler. */ }
98
+ }
99
+ }
100
+ }
101
+ if (this.#refreshPending) {
102
+ this.#refreshPending = false;
103
+ this.refresh();
104
+ }
105
+ } });
106
+ this.#drain();
107
+ }
108
+ onSnapshot(listener) {
109
+ this.#snapshots.add(listener);
110
+ this.#lastRevision = -1;
111
+ return () => this.#snapshots.delete(listener);
112
+ }
113
+ onFailure(listener) {
114
+ this.#failures.add(listener);
115
+ return () => this.#failures.delete(listener);
116
+ }
117
+ close() {
118
+ if (this.#closePromise !== undefined)
119
+ return this.#closePromise;
120
+ this.#closing = true;
121
+ if (this.#poll !== undefined)
122
+ clearInterval(this.#poll);
123
+ this.#closePromise = new Promise(resolve => {
124
+ if (!this.#started || this.#failed) {
125
+ resolve();
126
+ return;
127
+ }
128
+ const deadline = setTimeout(() => { this.#report("shutdown"); this.#fail("shutdown"); resolve(); }, 2000);
129
+ this.#queue.push({ request: { id: ++this.#sequence, kind: "close" }, done: (_value, ok) => {
130
+ if (!ok)
131
+ this.#report("shutdown");
132
+ const stopped = this.#worker?.terminate() ?? Promise.resolve();
133
+ void stopped.catch(() => { }).finally(() => { clearTimeout(deadline); resolve(); });
134
+ } });
135
+ this.#drain();
136
+ });
137
+ return this.#closePromise;
138
+ }
139
+ async #open() {
140
+ if (this.#failed)
141
+ return;
142
+ const { Worker } = await import("node:worker_threads");
143
+ if (this.#failed)
144
+ return;
145
+ const location = resolvePromptHistoryPath(this.options.dataDir, this.options.profileRoot);
146
+ const source = import.meta.url.endsWith(".ts");
147
+ const entry = new URL(source ? "./worker.ts" : "./worker.js", import.meta.url);
148
+ const options = { workerData: { ...location, limit: this.options.limit }, stdout: true, stderr: true };
149
+ this.#worker = this.options.createWorker?.() ?? (source
150
+ ? new Worker(`import('tsx/esm/api').then(({ tsImport }) => tsImport(${JSON.stringify(entry.href)}, ${JSON.stringify(import.meta.url)}))`, { ...options, eval: true })
151
+ : new Worker(entry, options));
152
+ this.#worker.stdout.resume();
153
+ this.#worker.stderr.resume();
154
+ this.#timer = setTimeout(() => this.#fail("unavailable"), 5000);
155
+ this.#worker.on("message", (message) => {
156
+ if (this.#failed)
157
+ return;
158
+ clearTimeout(this.#timer);
159
+ if (message === null || typeof message !== "object" || !Number.isSafeInteger(message.id) || typeof message.ok !== "boolean") {
160
+ this.#fail("unavailable");
161
+ return;
162
+ }
163
+ if (!message.ok) {
164
+ this.#fail(failureCode(message.code));
165
+ return;
166
+ }
167
+ if (message.id === 0) {
168
+ this.#ready = true;
169
+ if (!this.#closing) {
170
+ this.refresh();
171
+ this.#poll = setInterval(() => this.refresh(), 1000);
172
+ this.#poll.unref();
173
+ }
174
+ }
175
+ else {
176
+ const item = this.#active;
177
+ if (item?.request.id !== message.id) {
178
+ this.#fail("unavailable");
179
+ return;
180
+ }
181
+ this.#active = undefined;
182
+ item.done(message.value, true);
183
+ }
184
+ this.#drain();
185
+ });
186
+ this.#worker.on("error", () => this.#fail("unavailable"));
187
+ this.#worker.on("exit", () => { if (!this.#closing || this.#active !== undefined || this.#queue.length > 0)
188
+ this.#fail("unavailable"); });
189
+ }
190
+ #drain() {
191
+ if (!this.#ready || this.#failed || this.#active !== undefined)
192
+ return;
193
+ const next = this.#queue.shift();
194
+ if (next === undefined)
195
+ return;
196
+ this.#active = next;
197
+ this.#timer = setTimeout(() => this.#fail("busy"), 1500);
198
+ try {
199
+ this.#worker.postMessage(next.request);
200
+ }
201
+ catch {
202
+ this.#fail("unavailable");
203
+ }
204
+ }
205
+ #report(code) {
206
+ if (this.#reported.has(code))
207
+ return;
208
+ this.#reported.add(code);
209
+ for (const listener of this.#failures) {
210
+ try {
211
+ listener(code);
212
+ }
213
+ catch { /* Security: caller exceptions are not persistence diagnostics. */ }
214
+ }
215
+ }
216
+ #fail(code) {
217
+ if (this.#failed)
218
+ return;
219
+ this.#failed = true;
220
+ clearTimeout(this.#timer);
221
+ clearInterval(this.#poll);
222
+ this.#report(code);
223
+ const active = this.#active;
224
+ this.#active = undefined;
225
+ active?.done(undefined, false);
226
+ for (const item of this.#queue.splice(0))
227
+ item.done(undefined, false);
228
+ this.#worker?.unref();
229
+ void this.#worker?.terminate().catch(() => { });
230
+ }
231
+ }
232
+ function failureCode(value) {
233
+ switch (value) {
234
+ case "busy":
235
+ case "capacity":
236
+ case "oversized":
237
+ case "schema":
238
+ case "corrupt":
239
+ case "shutdown": return value;
240
+ default: return "unavailable";
241
+ }
242
+ }
243
+ function isSnapshot(value) {
244
+ if (typeof value !== "object" || value === null)
245
+ return false;
246
+ const candidate = value;
247
+ if (!Number.isSafeInteger(candidate.revision) || candidate.revision < 0 || !Number.isInteger(candidate.limit)
248
+ || candidate.limit < 10 || candidate.limit > 100 || candidate.limit % 10 !== 0
249
+ || !Array.isArray(candidate.entries) || candidate.entries.length > candidate.limit)
250
+ return false;
251
+ let bytes = 0;
252
+ return candidate.entries.every(item => {
253
+ if (typeof item?.text !== "string" || item.text !== item.text.trim()
254
+ || typeof item.submissionId !== "string" || item.submissionId.length > 128)
255
+ return false;
256
+ const size = Buffer.byteLength(item.text);
257
+ bytes += size;
258
+ return size > 0 && size <= PROMPT_HISTORY_MAX_ENTRY_BYTES && bytes <= PROMPT_HISTORY_MAX_TEXT_BYTES;
259
+ });
260
+ }
@@ -0,0 +1,15 @@
1
+ import { type PromptHistoryFailure, type PromptHistorySnapshot, type PromptHistorySubmission } from "../../contracts/owned-ui/index.js";
2
+ export declare class HistoryStorageError extends Error {
3
+ readonly code: PromptHistoryFailure;
4
+ constructor(code: PromptHistoryFailure);
5
+ }
6
+ export declare class PromptHistoryStore {
7
+ #private;
8
+ readonly path: string;
9
+ readonly profileId: string;
10
+ constructor(path: string, profileId: string, limit: number);
11
+ record(input: PromptHistorySubmission): void;
12
+ snapshot(): PromptHistorySnapshot;
13
+ close(): void;
14
+ }
15
+ export declare function classifyHistoryError(error: unknown): PromptHistoryFailure;