forgeos 0.1.0-alpha.24 → 0.1.0-alpha.25

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/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.24 input=1257771517e723f30d9c8c6bdc328f3a9d0bfde1e673d859837f93f0f1bd15c4 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
1
+ // @forge-generated generator=0.1.0-alpha.25 input=f4bc0a51dfadbdbabe4e7083c29844716f74c2beebcb86ce30e7a70c4f9487b4 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.0-alpha.25
6
+
7
+ ### Patch Changes
8
+
9
+ - Harden DeltaDB and Agent Memory under real `forge dev` concurrency.
10
+
11
+ - Stop long-running dev recorders from holding the DeltaDB writer lock between events.
12
+ - Retry short transient DeltaDB writer conflicts before reporting `FORGE_DELTA_BUSY`.
13
+ - Keep Codex hook queue checkpoints unchanged when Agent Memory ingest is blocked by a busy DeltaDB writer, then retry safely instead of losing queued events.
14
+ - Add watcher backoff metadata for lock recovery and document the safe queue/DeltaDB behavior.
15
+
16
+ - Fix tenant-scope reporting in the generated agent contract and capability map.
17
+
18
+ - Match tenant-scoped tables by both authored/camelCase table names and generated SQL snake_case table names.
19
+ - Report camelCase liveQuery dependencies such as `onboardingTasks` as `tenant` scoped when `tenantScope.json` confirms `tenant_id`.
20
+ - Add regression coverage for the Team Onboarding style liveQuery/capability-map path.
21
+
5
22
  ## 0.1.0-alpha.24
6
23
 
7
24
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,19 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.25
10
+
11
+ - Hardened DeltaDB and Agent Memory for concurrent `forge dev` usage:
12
+ dev recorders now release the writer lock between events, Agent Memory ingest
13
+ retries short transient writer conflicts, and queued Codex hook events keep
14
+ their checkpoint unchanged when DeltaDB is temporarily busy.
15
+ - Fixed tenant-scope reporting in the generated agent contract and capability
16
+ map for camelCase authored tables such as `onboardingTasks`, so liveQuery
17
+ dependencies now report `tenant` scope when `tenantScope.json` confirms the
18
+ table is tenant-scoped.
19
+ - Added regression tests and docs for the DeltaDB lock recovery path and the
20
+ Team Onboarding style capability-map tenant-scope path.
21
+
9
22
  ## 0.1.0-alpha.24
10
23
 
11
24
  - Consolidated the public alpha adoption surface: MIT license, package license
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.24",
3
+ "version": "0.1.0-alpha.25",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.24","releaseId":"forgeos@0.1.0-alpha.24+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.25","releaseId":"forgeos@0.1.0-alpha.25+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.24 input=1257771517e723f30d9c8c6bdc328f3a9d0bfde1e673d859837f93f0f1bd15c4 content=4170e33ff26cbaa55732ed4f507f83f2ece3812a1c2184614337c8be91110499
1
+ // @forge-generated generator=0.1.0-alpha.25 input=f4bc0a51dfadbdbabe4e7083c29844716f74c2beebcb86ce30e7a70c4f9487b4 content=a7c5e9f36b973e9b5a51716753739f57a63fa77c68b0fad18f059b5407aa58fe
2
2
  export const releaseManifest = {
3
3
  "defaultProvider": "local",
4
4
  "diagnostics": [],
@@ -19,7 +19,7 @@ export const releaseManifest = {
19
19
  "custom"
20
20
  ],
21
21
  "packageName": "forgeos",
22
- "packageVersion": "0.1.0-alpha.24",
23
- "releaseId": "forgeos@0.1.0-alpha.24+unknown",
22
+ "packageVersion": "0.1.0-alpha.25",
23
+ "releaseId": "forgeos@0.1.0-alpha.25+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -113,10 +113,16 @@ async function openMemoryStore(
113
113
  workspaceRoot: string,
114
114
  access: "read" | "write" = "write",
115
115
  ): Promise<DeltaStore | AgentMemoryUnavailableResult> {
116
- try {
117
- return await DeltaStore.open(workspaceRoot, { access });
118
- } catch (error) {
119
- return memoryUnavailable(error, workspaceRoot);
116
+ const retryDelays = access === "write" ? [25, 75, 150] : [];
117
+ for (let attempt = 0; ; attempt += 1) {
118
+ try {
119
+ return await DeltaStore.open(workspaceRoot, { access });
120
+ } catch (error) {
121
+ if (!(error instanceof DeltaStoreBusyError) || attempt >= retryDelays.length) {
122
+ return memoryUnavailable(error, workspaceRoot);
123
+ }
124
+ await sleep(retryDelays[attempt] ?? 0);
125
+ }
120
126
  }
121
127
  }
122
128
 
@@ -131,6 +137,10 @@ function isExternalPgliteRead(result: AgentMemoryUnavailableResult): boolean {
131
137
  );
132
138
  }
133
139
 
140
+ function isDeltaBusyIngestResult(result: AgentIngestResult): boolean {
141
+ return result.ok === false && result.busy?.code === "FORGE_DELTA_BUSY";
142
+ }
143
+
134
144
  function fallbackMemoryPath(workspaceRoot: string): string {
135
145
  return join(workspaceRoot, ".forge", "agent", "events.ndjson");
136
146
  }
@@ -413,23 +423,29 @@ async function ingestAgentMemoryQueueFile(options: AgentMemoryCommandOptions): P
413
423
  source,
414
424
  eventName: options.eventName,
415
425
  });
426
+ const errors = [
427
+ ...drained.errors,
428
+ ...(drained.busy ? ["DeltaDB is busy; queue checkpoint was not advanced"] : []),
429
+ ];
416
430
  return {
417
- ok: drained.errors.length === 0,
431
+ ok: errors.length === 0,
418
432
  watch: false,
419
433
  source,
420
434
  file: options.file,
421
435
  eventsIngested: drained.eventsIngested,
422
- errors: drained.errors,
436
+ errors,
423
437
  bytesRead: drained.bytesRead,
424
438
  pendingBytes: drained.pendingBytes,
425
439
  checkpointFile: drained.checkpointFile,
426
440
  compacted: drained.compacted,
427
441
  historyFile: drained.historyFile,
442
+ ...(drained.busy ? { busy: drained.busy, pendingDueToBusy: true } : {}),
428
443
  nextActions: [
444
+ ...(drained.busy ? ["forge delta status --json"] : []),
429
445
  `forge agent memory --entry ${source} --json`,
430
446
  `forge agent hooks status --target ${source} --json`,
431
447
  ],
432
- exitCode: drained.errors.length === 0 ? 0 : 1,
448
+ exitCode: errors.length === 0 ? 0 : 1,
433
449
  };
434
450
  }
435
451
 
@@ -644,6 +660,7 @@ export async function drainAgentMemoryQueueFile(options: {
644
660
  checkpointFile: string;
645
661
  compacted: boolean;
646
662
  historyFile: string;
663
+ busy?: AgentMemoryUnavailableResult["busy"];
647
664
  }> {
648
665
  const historyFile = queueHistoryPath(options.watchFile);
649
666
  if (!existsSync(options.watchFile)) {
@@ -699,6 +716,17 @@ export async function drainAgentMemoryQueueFile(options: {
699
716
  eventsIngested += 1;
700
717
  consumedOffset = bytesRead + line.endOffset;
701
718
  writeQueueCheckpoint(options.watchFile, consumedOffset);
719
+ } else if (isDeltaBusyIngestResult(result)) {
720
+ return {
721
+ eventsIngested,
722
+ errors,
723
+ bytesRead,
724
+ pendingBytes,
725
+ checkpointFile: queueCheckpointPath(options.watchFile),
726
+ compacted: false,
727
+ historyFile,
728
+ busy: result.busy,
729
+ };
702
730
  } else {
703
731
  errors.push(result.error ?? "agent memory ingest failed");
704
732
  break;
@@ -886,7 +914,19 @@ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promi
886
914
 
887
915
  let eventsIngested = 0;
888
916
  const errors: string[] = [];
917
+ let busyRetries = 0;
918
+ let lastBusy: AgentMemoryUnavailableResult["busy"] | undefined;
889
919
  let pendingIngest = Promise.resolve();
920
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
921
+ const scheduleBusyRetry = () => {
922
+ if (retryTimer) {
923
+ return;
924
+ }
925
+ retryTimer = setTimeout(() => {
926
+ retryTimer = undefined;
927
+ pendingIngest = pendingIngest.then(ingestNewContent, ingestNewContent);
928
+ }, 500);
929
+ };
890
930
  const ingestNewContent = async () => {
891
931
  const result = await drainAgentMemoryQueueFile({
892
932
  workspaceRoot: options.workspaceRoot,
@@ -895,6 +935,13 @@ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promi
895
935
  eventName: options.eventName,
896
936
  });
897
937
  eventsIngested += result.eventsIngested;
938
+ if (result.busy) {
939
+ busyRetries += 1;
940
+ lastBusy = result.busy;
941
+ scheduleBusyRetry();
942
+ return;
943
+ }
944
+ lastBusy = undefined;
898
945
  errors.push(...result.errors);
899
946
  };
900
947
 
@@ -904,6 +951,10 @@ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promi
904
951
  pendingIngest = pendingIngest.then(ingestNewContent, ingestNewContent);
905
952
  });
906
953
  const shutdown = () => {
954
+ if (retryTimer) {
955
+ clearTimeout(retryTimer);
956
+ retryTimer = undefined;
957
+ }
907
958
  watcher.close();
908
959
  void pendingIngest.finally(() => {
909
960
  resolve({
@@ -913,7 +964,9 @@ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promi
913
964
  file,
914
965
  eventsIngested,
915
966
  errors,
967
+ ...(lastBusy ? { busy: lastBusy, pendingDueToBusy: true, busyRetries } : {}),
916
968
  nextActions: [
969
+ ...(lastBusy ? ["forge delta status --json"] : []),
917
970
  `forge agent memory --entry ${source} --json`,
918
971
  `forge agent hooks status --target ${source} --json`,
919
972
  ],
@@ -971,6 +1024,10 @@ function installAgentMemory(options: AgentMemoryCommandOptions): AgentInstallRes
971
1024
  return cursorInstallResult(filesWritten, planned);
972
1025
  }
973
1026
 
1027
+ function sleep(ms: number): Promise<void> {
1028
+ return new Promise((resolve) => setTimeout(resolve, ms));
1029
+ }
1030
+
974
1031
  export function formatAgentMemoryJson(result: AgentMemoryCommandResult): string {
975
1032
  return `${JSON.stringify(result, null, 2)}\n`;
976
1033
  }
@@ -155,6 +155,7 @@ export interface AgentIngestResult {
155
155
  path: string;
156
156
  reason: "pglite-active";
157
157
  };
158
+ busy?: AgentMemoryUnavailableResult["busy"];
158
159
  exitCode: 0 | 1;
159
160
  error?: string;
160
161
  diagnostics?: Diagnostic[];
@@ -174,6 +175,9 @@ export interface AgentIngestWatchResult {
174
175
  checkpointFile?: string;
175
176
  compacted?: boolean;
176
177
  historyFile?: string;
178
+ busy?: AgentMemoryUnavailableResult["busy"];
179
+ pendingDueToBusy?: boolean;
180
+ busyRetries?: number;
177
181
  nextActions: string[];
178
182
  exitCode: 0 | 1;
179
183
  }
@@ -7,6 +7,7 @@ import { detectSecrets } from "../classifier/secrets.ts";
7
7
  import { GENERATOR_VERSION } from "../emitter/constants.ts";
8
8
  import { stripDeterministicHeader } from "../primitives/header.ts";
9
9
  import { canonicalJson, normalizeNewlines, serializeCanonical } from "../primitives/serialize.ts";
10
+ import { toSnakeCase } from "../data-graph/sql/naming.ts";
10
11
  import { resolveByPackageName } from "../recipes/registry.ts";
11
12
  import {
12
13
  defaultRuntimeCompatibility,
@@ -288,6 +289,35 @@ function runtimeRules(): AgentRuntimeRule[] {
288
289
  ];
289
290
  }
290
291
 
292
+ type TenantTableInfo = {
293
+ tenantIdColumn: string;
294
+ tenantField: string;
295
+ };
296
+
297
+ function buildTenantTableLookup(tenantScope: TenantScope, dataGraph: DataGraph): Map<string, TenantTableInfo> {
298
+ const lookup = new Map<string, TenantTableInfo>();
299
+ for (const scoped of tenantScope.tables) {
300
+ const table = dataGraph.tables.find((candidate) =>
301
+ candidate.name === scoped.table ||
302
+ candidate.exportName === scoped.exportName ||
303
+ toSnakeCase(candidate.name) === scoped.table ||
304
+ toSnakeCase(candidate.exportName) === scoped.table
305
+ );
306
+ const tenantField = table?.fields.find((field) => toSnakeCase(field.name) === scoped.tenantIdColumn)?.name ??
307
+ scoped.tenantIdColumn;
308
+ const info = { tenantIdColumn: scoped.tenantIdColumn, tenantField };
309
+ for (const key of uniqueSorted([
310
+ scoped.table,
311
+ scoped.exportName,
312
+ table?.name,
313
+ table?.exportName,
314
+ ].filter((key): key is string => typeof key === "string" && key.length > 0))) {
315
+ lookup.set(key, info);
316
+ }
317
+ }
318
+ return lookup;
319
+ }
320
+
291
321
  function agentProtocols(workspaceRoot: string): AgentProtocolInfo[] {
292
322
  return [
293
323
  {
@@ -1037,9 +1067,7 @@ export function buildAgentContractArtifacts(
1037
1067
  input: AgentContractInput,
1038
1068
  ): AgentContractArtifacts {
1039
1069
  const project = readPackageInfo(input.workspaceRoot);
1040
- const tenantTables = new Map(
1041
- input.tenantScope.tables.map((table) => [table.table, table.tenantIdColumn]),
1042
- );
1070
+ const tenantTables = buildTenantTableLookup(input.tenantScope, input.dataGraph);
1043
1071
  const commandAuth = new Map(
1044
1072
  input.policyRegistry.commandAuth.map((binding) => [binding.commandName, binding.auth]),
1045
1073
  );
@@ -1183,13 +1211,16 @@ export function buildAgentContractArtifacts(
1183
1211
  }),
1184
1212
  ),
1185
1213
  data: {
1186
- tables: sorted(input.dataGraph.tables, (table) => table.name).map((table) => ({
1187
- name: table.name,
1188
- file: table.file,
1189
- tenantScoped: tenantTables.has(table.name),
1190
- ...(tenantTables.has(table.name) ? { tenantField: tenantTables.get(table.name) } : {}),
1191
- fields: uniqueSorted(table.fields.map((field) => field.name)),
1192
- })),
1214
+ tables: sorted(input.dataGraph.tables, (table) => table.name).map((table) => {
1215
+ const tenantInfo = tenantTables.get(table.name);
1216
+ return {
1217
+ name: table.name,
1218
+ file: table.file,
1219
+ tenantScoped: Boolean(tenantInfo),
1220
+ ...(tenantInfo ? { tenantField: tenantInfo.tenantField } : {}),
1221
+ fields: uniqueSorted(table.fields.map((field) => field.name)),
1222
+ };
1223
+ }),
1193
1224
  },
1194
1225
  policies: sorted(input.policyRegistry.policies, (policy) => policy.name).map((policy) => ({
1195
1226
  name: policy.name,
@@ -4,7 +4,7 @@ import type { ForgeCommand } from "../cli/parse.ts";
4
4
  import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
5
5
  import { normalizePath } from "../compiler/primitives/paths.ts";
6
6
  import { hashUtf8Bytes } from "../compiler/primitives/hash.ts";
7
- import { DeltaStore, type DeltaRuntimeCallInput } from "./store.ts";
7
+ import { DeltaStore, DeltaStoreBusyError, type DeltaRuntimeCallInput } from "./store.ts";
8
8
  import { classifyArtifactKind } from "./classifier.ts";
9
9
  import { readDeltaGitSnapshot } from "./git-observer.ts";
10
10
 
@@ -28,70 +28,129 @@ export async function createAmbientDeltaRecorder(
28
28
  if (isDeltaDisabled()) {
29
29
  return noopRecorder;
30
30
  }
31
- try {
32
- const store = await DeltaStore.open(workspaceRoot);
33
- const actorId = await store.ensureActor("forge", "forge-cli", { pid: process.pid });
34
- const sessionId = await store.createSession({
31
+ let actorId: string | undefined;
32
+ let sessionId: string | undefined;
33
+ let accepting = true;
34
+ let closed = false;
35
+ let queue = Promise.resolve();
36
+
37
+ const withStore = async (fn: (store: DeltaStore) => Promise<void>): Promise<void> => {
38
+ const store = await openDeltaStoreWithRetry(workspaceRoot);
39
+ try {
40
+ await fn(store);
41
+ } finally {
42
+ await store.close();
43
+ }
44
+ };
45
+
46
+ const enqueue = (fn: (store: DeltaStore) => Promise<void>): Promise<void> => {
47
+ queue = queue.then(() => safeDelta(() => withStore(fn)));
48
+ return queue;
49
+ };
50
+
51
+ const ensureSession = async (store: DeltaStore): Promise<{ actorId: string; sessionId: string } | null> => {
52
+ if (actorId && sessionId) {
53
+ return { actorId, sessionId };
54
+ }
55
+ actorId = await store.ensureActor("forge", "forge-cli", { pid: process.pid });
56
+ sessionId = await store.createSession({
35
57
  source,
36
58
  summary,
37
59
  metadata: { actorId },
38
60
  git: readDeltaGitSnapshot(workspaceRoot),
39
61
  });
40
- return {
41
- sessionId,
42
- async recordRuntimeCall(input) {
43
- await safeDelta(async () => {
44
- const failedCode = input.diagnosticCode ?? diagnosticCode(input.diagnostics);
45
- await store.appendOperation({
46
- sessionId,
47
- actorId,
48
- kind: input.result === "denied"
49
- ? "runtime.entry.denied"
50
- : input.result === "failed"
51
- ? "runtime.entry.failed"
52
- : "runtime.entry.executed",
53
- summary: `${input.entryName} ${input.result ?? "executed"}`,
54
- data: {
55
- entryName: input.entryName,
56
- entryKind: input.entryKind,
57
- result: input.result,
58
- traceId: input.traceId,
59
- diagnosticCode: failedCode,
60
- },
61
- runtimeCall: { ...input, diagnosticCode: failedCode },
62
- });
63
- });
64
- },
65
- async recordAgentTool(input) {
66
- await safeDelta(async () => {
67
- await store.appendOperation({
68
- sessionId,
69
- actorId,
70
- kind: "agent.tool.called",
71
- summary: `${input.toolName} ${input.status}`,
72
- data: {
73
- toolName: input.toolName,
74
- risk: input.risk,
75
- status: input.status,
76
- traceId: input.traceId,
77
- durationMs: input.durationMs,
78
- },
79
- });
62
+ return { actorId, sessionId };
63
+ };
64
+
65
+ await enqueue(async (store) => {
66
+ await ensureSession(store);
67
+ });
68
+
69
+ return {
70
+ get sessionId() {
71
+ return sessionId;
72
+ },
73
+ async recordRuntimeCall(input) {
74
+ if (!accepting) {
75
+ return;
76
+ }
77
+ await enqueue(async (store) => {
78
+ const session = await ensureSession(store);
79
+ if (!session) {
80
+ return;
81
+ }
82
+ const failedCode = input.diagnosticCode ?? diagnosticCode(input.diagnostics);
83
+ await store.appendOperation({
84
+ sessionId: session.sessionId,
85
+ actorId: session.actorId,
86
+ kind: input.result === "denied"
87
+ ? "runtime.entry.denied"
88
+ : input.result === "failed"
89
+ ? "runtime.entry.failed"
90
+ : "runtime.entry.executed",
91
+ summary: `${input.entryName} ${input.result ?? "executed"}`,
92
+ data: {
93
+ entryName: input.entryName,
94
+ entryKind: input.entryKind,
95
+ result: input.result,
96
+ traceId: input.traceId,
97
+ diagnosticCode: failedCode,
98
+ },
99
+ runtimeCall: { ...input, diagnosticCode: failedCode },
80
100
  });
81
- },
82
- async recordFileChanged(path, changeType = "modified") {
83
- await safeDelta(() => store.recordFilePath(sessionId, path, changeType));
84
- },
85
- async close(closeSummary) {
86
- await safeDelta(async () => {
87
- await store.endSession(sessionId, closeSummary);
88
- await store.close();
101
+ });
102
+ },
103
+ async recordAgentTool(input) {
104
+ if (!accepting) {
105
+ return;
106
+ }
107
+ await enqueue(async (store) => {
108
+ const session = await ensureSession(store);
109
+ if (!session) {
110
+ return;
111
+ }
112
+ await store.appendOperation({
113
+ sessionId: session.sessionId,
114
+ actorId: session.actorId,
115
+ kind: "agent.tool.called",
116
+ summary: `${input.toolName} ${input.status}`,
117
+ data: {
118
+ toolName: input.toolName,
119
+ risk: input.risk,
120
+ status: input.status,
121
+ traceId: input.traceId,
122
+ durationMs: input.durationMs,
123
+ },
89
124
  });
90
- },
91
- };
92
- } catch {
93
- return noopRecorder;
94
- }
125
+ });
126
+ },
127
+ async recordFileChanged(path, changeType = "modified") {
128
+ if (!accepting) {
129
+ return;
130
+ }
131
+ await enqueue(async (store) => {
132
+ const session = await ensureSession(store);
133
+ if (!session) {
134
+ return;
135
+ }
136
+ await store.recordFilePath(session.sessionId, path, changeType);
137
+ });
138
+ },
139
+ async close(closeSummary) {
140
+ if (closed) {
141
+ await queue;
142
+ return;
143
+ }
144
+ accepting = false;
145
+ closed = true;
146
+ await enqueue(async (store) => {
147
+ if (!sessionId) {
148
+ return;
149
+ }
150
+ await store.endSession(sessionId, closeSummary);
151
+ });
152
+ },
153
+ };
95
154
  }
96
155
 
97
156
  export async function recordParsedCliCommand(input: {
@@ -343,6 +402,25 @@ const noopRecorder: AmbientDeltaRecorder = {
343
402
  async close() {},
344
403
  };
345
404
 
405
+ const DELTA_STORE_RETRY_DELAYS_MS = [25, 75, 150];
406
+
407
+ async function openDeltaStoreWithRetry(workspaceRoot: string): Promise<DeltaStore> {
408
+ for (let attempt = 0; ; attempt += 1) {
409
+ try {
410
+ return await DeltaStore.open(workspaceRoot);
411
+ } catch (error) {
412
+ if (!(error instanceof DeltaStoreBusyError) || attempt >= DELTA_STORE_RETRY_DELAYS_MS.length) {
413
+ throw error;
414
+ }
415
+ await sleep(DELTA_STORE_RETRY_DELAYS_MS[attempt] ?? 0);
416
+ }
417
+ }
418
+ }
419
+
420
+ function sleep(ms: number): Promise<void> {
421
+ return new Promise((resolve) => setTimeout(resolve, ms));
422
+ }
423
+
346
424
  async function safeDelta(fn: () => Promise<void>): Promise<void> {
347
425
  try {
348
426
  await fn();
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.24";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.25";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;