salmon-loop 0.2.16 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/cli/authorization/non-interactive.js +7 -21
  2. package/dist/cli/commands/chat.js +1 -1
  3. package/dist/cli/commands/parallel.js +46 -41
  4. package/dist/cli/commands/run/assistant-message.js +3 -0
  5. package/dist/cli/commands/run/handler.js +2 -1
  6. package/dist/cli/commands/serve.js +112 -156
  7. package/dist/cli/headless/json-protocol.js +1 -1
  8. package/dist/cli/headless/stream-json-protocol.js +3 -2
  9. package/dist/cli/program-bootstrap.js +2 -2
  10. package/dist/cli/slash/runtime.js +5 -1
  11. package/dist/core/adapters/fs/node-fs.js +1 -0
  12. package/dist/core/backends/salmon-loop/task-executor.js +1 -0
  13. package/dist/core/benchmark/patch-artifact.js +1 -1
  14. package/dist/core/context/service.js +5 -2
  15. package/dist/core/extensions/index.js +2 -35
  16. package/dist/core/extensions/merge.js +14 -0
  17. package/dist/core/extensions/redact.js +9 -3
  18. package/dist/core/extensions/schemas.js +2 -51
  19. package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
  20. package/dist/core/facades/cli-program-bootstrap.js +1 -0
  21. package/dist/core/facades/cli-serve.js +2 -1
  22. package/dist/core/grizzco/dsl/strategies.js +1 -3
  23. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
  24. package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
  25. package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
  26. package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
  27. package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
  28. package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
  29. package/dist/core/grizzco/steps/apply.js +0 -7
  30. package/dist/core/grizzco/steps/autopilot.js +108 -6
  31. package/dist/core/grizzco/steps/preflight.js +10 -0
  32. package/dist/core/grizzco/steps/tool-runtime.js +1 -0
  33. package/dist/core/interaction/events/bus.js +14 -0
  34. package/dist/core/interaction/orchestration/facade.js +11 -1
  35. package/dist/core/llm/ai-sdk/request-params.js +40 -1
  36. package/dist/core/mcp/bridge/index.js +4 -0
  37. package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
  38. package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
  39. package/dist/core/mcp/bridge/tool-bridge.js +303 -0
  40. package/dist/core/mcp/cache/resource-cache.js +41 -0
  41. package/dist/core/mcp/catalog/discovery.js +51 -0
  42. package/dist/core/mcp/catalog/notification-router.js +28 -0
  43. package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
  44. package/dist/core/mcp/catalog/resource-catalog.js +7 -0
  45. package/dist/core/mcp/catalog/tool-catalog.js +4 -0
  46. package/dist/core/mcp/client/connection-manager.js +239 -0
  47. package/dist/core/mcp/client/lifecycle.js +13 -0
  48. package/dist/core/mcp/client/transport-factory.js +168 -0
  49. package/dist/core/mcp/config/index.js +32 -0
  50. package/dist/core/mcp/config/schema-v2.js +129 -0
  51. package/dist/core/mcp/host/elicitation-provider.js +209 -0
  52. package/dist/core/mcp/host/roots-provider.js +70 -0
  53. package/dist/core/mcp/host/sampling-provider.js +170 -0
  54. package/dist/core/mcp/index.js +4 -0
  55. package/dist/core/mcp/observability/events.js +19 -0
  56. package/dist/core/mcp/policy/approval-policy.js +2 -0
  57. package/dist/core/mcp/policy/classifier.js +172 -0
  58. package/dist/core/mcp/policy/grants.js +356 -0
  59. package/dist/core/mcp/policy/uri-policy.js +60 -0
  60. package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
  61. package/dist/core/mcp/types.js +2 -0
  62. package/dist/core/protocols/a2a/agent-card.js +38 -12
  63. package/dist/core/protocols/a2a/sdk/executor.js +105 -36
  64. package/dist/core/protocols/a2a/sdk/server.js +1311 -3
  65. package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
  66. package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
  67. package/dist/core/protocols/acp/acp-types.js +17 -0
  68. package/dist/core/protocols/acp/formal-agent.js +389 -502
  69. package/dist/core/protocols/acp/handlers.js +3 -0
  70. package/dist/core/protocols/acp/permission-provider.js +11 -39
  71. package/dist/core/protocols/acp/stdio-server.js +20 -1
  72. package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
  73. package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
  74. package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
  75. package/dist/core/public-capabilities/projections.js +1 -0
  76. package/dist/core/runtime/agent-server-runtime.js +2 -3
  77. package/dist/core/runtime/spawn-command.js +8 -2
  78. package/dist/core/runtime/spawn-interactive.js +26 -0
  79. package/dist/core/session/manager.js +48 -25
  80. package/dist/core/tools/builtin/index.js +6 -1
  81. package/dist/core/tools/builtin/proposal.js +0 -7
  82. package/dist/core/tools/builtin/workspace.js +76 -0
  83. package/dist/core/tools/dispatcher.js +1 -0
  84. package/dist/core/tools/loader.js +92 -46
  85. package/dist/core/verification/runner.js +60 -31
  86. package/dist/core/version.js +17 -0
  87. package/dist/core/workspace/capabilities.js +80 -0
  88. package/dist/locales/en.js +17 -3
  89. package/package.json +4 -2
  90. package/dist/core/protocols/a2a/mapper.js +0 -14
  91. package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
  92. package/dist/core/protocols/a2a/task-projection.js +0 -45
  93. package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
  94. package/dist/core/tools/mcp/client.js +0 -308
  95. package/dist/core/tools/mcp/loader.js +0 -110
  96. package/dist/core/tools/mcp/schema.js +0 -54
  97. package/dist/core/tools/mcp/streamable-http.js +0 -101
  98. package/dist/core/tools/mcp/types.js +0 -26
@@ -0,0 +1,113 @@
1
+ import { text } from '../../../locales/index.js';
2
+ import { recordAuditEvent } from '../../observability/audit-trail.js';
3
+ import { hashRepoPath } from './acp-types.js';
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+ function toCheckpointMeta(input) {
8
+ if (!input)
9
+ return null;
10
+ return {
11
+ id: input.id,
12
+ createdAt: input.createdAt ?? null,
13
+ strategy: input.strategy ?? null,
14
+ backend: input.backend ?? null,
15
+ };
16
+ }
17
+ function toResumeHint(probe) {
18
+ if (!probe || probe.valid)
19
+ return null;
20
+ switch (probe.reason) {
21
+ case 'not_found':
22
+ return {
23
+ code: 'CHECKPOINT_NOT_FOUND',
24
+ message: text.acp.checkpointNotFound,
25
+ };
26
+ case 'manifest_parse_error':
27
+ return {
28
+ code: 'CHECKPOINT_MANIFEST_PARSE_ERROR',
29
+ message: text.acp.checkpointManifestParseError,
30
+ };
31
+ case 'manifest_io_error':
32
+ return {
33
+ code: 'CHECKPOINT_MANIFEST_IO_ERROR',
34
+ message: text.acp.checkpointManifestIoError,
35
+ };
36
+ case 'manifest_lock_timeout':
37
+ return {
38
+ code: 'CHECKPOINT_MANIFEST_LOCK_TIMEOUT',
39
+ message: text.acp.checkpointManifestLockTimeout,
40
+ };
41
+ case 'manifest_unavailable':
42
+ return {
43
+ code: 'CHECKPOINT_MANIFEST_UNAVAILABLE',
44
+ message: text.acp.checkpointManifestUnavailable,
45
+ };
46
+ default:
47
+ return {
48
+ code: 'CHECKPOINT_RESUME_UNAVAILABLE',
49
+ message: text.acp.checkpointResumeUnavailable,
50
+ };
51
+ }
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Public API
55
+ // ---------------------------------------------------------------------------
56
+ export async function probeCheckpoint(reader, params) {
57
+ const startedAt = Date.now();
58
+ const checkpoints = await reader.listBySession({
59
+ repoPath: params.repoPath,
60
+ sessionId: params.sessionId,
61
+ limit: 1,
62
+ });
63
+ const latest = checkpoints.at(-1);
64
+ let resumeProbe = null;
65
+ if (latest?.id && reader.probeById) {
66
+ const probed = await reader.probeById({
67
+ repoPath: params.repoPath,
68
+ checkpointId: latest.id,
69
+ });
70
+ resumeProbe = {
71
+ checkpointId: latest.id,
72
+ valid: probed.valid,
73
+ reason: probed.reason,
74
+ };
75
+ }
76
+ else if (latest?.id && reader.getById) {
77
+ const found = await reader.getById({
78
+ repoPath: params.repoPath,
79
+ checkpointId: latest.id,
80
+ });
81
+ resumeProbe = {
82
+ checkpointId: latest.id,
83
+ valid: Boolean(found),
84
+ reason: found ? 'ok' : 'not_found',
85
+ };
86
+ }
87
+ const resumeReady = resumeProbe?.valid ?? Boolean(latest);
88
+ recordAuditEvent('acp.checkpoint.read', {
89
+ sessionId: params.sessionId,
90
+ repoPathHash: params.repoPathHash,
91
+ latestCheckpointId: latest?.id ?? null,
92
+ hit: Boolean(latest),
93
+ latencyMs: Date.now() - startedAt,
94
+ resumeProbe: resumeProbe ?? undefined,
95
+ }, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
96
+ const resumeHint = toResumeHint(resumeProbe);
97
+ return {
98
+ _meta: {
99
+ salmonloop: {
100
+ latestCheckpointId: latest?.id ?? null,
101
+ checkpoint: toCheckpointMeta(latest),
102
+ resumeReady,
103
+ resumeProbe,
104
+ resumeHint: resumeHint?.message ?? null,
105
+ resumeHintCode: resumeHint?.code ?? null,
106
+ },
107
+ },
108
+ };
109
+ }
110
+ export function probeCheckpointForNewSession(reader, params) {
111
+ return probeCheckpoint(reader, { ...params, repoPathHash: hashRepoPath(params.repoPath) });
112
+ }
113
+ //# sourceMappingURL=acp-checkpoint-probe.js.map
@@ -0,0 +1,336 @@
1
+ import { mkdir, open, readFile, rename, stat, unlink, writeFile, } from '../../adapters/fs/node-fs.js';
2
+ import { defaultPathAdapter } from '../../adapters/path/path-adapter.js';
3
+ import { recordAuditEvent } from '../../observability/audit-trail.js';
4
+ import { hashRepoPath, isPermissionPolicyValue, parseTimestamp, } from './acp-types.js';
5
+ export function createAcpSessionPersistence(options) {
6
+ const deletedSessionIds = new Map();
7
+ let sessionsHydrated = false;
8
+ let hydratePromise = null;
9
+ // -------------------------------------------------------------------------
10
+ // Helpers
11
+ // -------------------------------------------------------------------------
12
+ function isPidAlive(pid) {
13
+ if (!Number.isInteger(pid) || pid <= 0)
14
+ return false;
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch (error) {
20
+ if (error &&
21
+ typeof error === 'object' &&
22
+ 'code' in error &&
23
+ error.code === 'EPERM') {
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ }
29
+ function isFileMissing(error) {
30
+ return Boolean(error &&
31
+ typeof error === 'object' &&
32
+ 'code' in error &&
33
+ (error.code === 'ENOENT' ||
34
+ error.code === 'ENOTDIR'));
35
+ }
36
+ function pruneSessionRecords(records) {
37
+ const cutoff = Date.now() - options.storePolicy.maxAgeMs;
38
+ return [...records]
39
+ .filter((record) => parseTimestamp(record.updatedAt) >= cutoff)
40
+ .sort((a, b) => parseTimestamp(b.updatedAt) - parseTimestamp(a.updatedAt))
41
+ .slice(0, options.storePolicy.maxEntries);
42
+ }
43
+ function normalizeDeletedSessionRecords(input) {
44
+ if (!Array.isArray(input))
45
+ return [];
46
+ const byId = new Map();
47
+ for (const entry of input) {
48
+ if (!entry || typeof entry !== 'object')
49
+ continue;
50
+ const record = entry;
51
+ if (typeof record.id !== 'string' || !record.id)
52
+ continue;
53
+ if (typeof record.deletedAt !== 'string' || !record.deletedAt)
54
+ continue;
55
+ const current = byId.get(record.id);
56
+ if (!current || parseTimestamp(record.deletedAt) > parseTimestamp(current.deletedAt)) {
57
+ byId.set(record.id, { id: record.id, deletedAt: record.deletedAt });
58
+ }
59
+ }
60
+ return Array.from(byId.values());
61
+ }
62
+ function pruneDeletedSessionRecords(records) {
63
+ const cutoff = Date.now() - options.storePolicy.maxAgeMs;
64
+ return normalizeDeletedSessionRecords(records)
65
+ .filter((record) => parseTimestamp(record.deletedAt) >= cutoff)
66
+ .sort((a, b) => parseTimestamp(b.deletedAt) - parseTimestamp(a.deletedAt));
67
+ }
68
+ function normalizePersistedSessionStore(input) {
69
+ if (!input || typeof input !== 'object') {
70
+ return { schemaVersion: 2, sessions: [] };
71
+ }
72
+ const raw = input;
73
+ if (!Array.isArray(raw.sessions))
74
+ return { schemaVersion: 2, sessions: [] };
75
+ if (raw.schemaVersion === 1) {
76
+ return {
77
+ schemaVersion: 2,
78
+ sessions: raw.sessions.map((entry) => ({
79
+ id: entry.id,
80
+ cwd: entry.cwd,
81
+ mcpServers: entry.mcpServers,
82
+ createdAt: entry.createdAt,
83
+ updatedAt: entry.updatedAt,
84
+ title: entry.title,
85
+ taskId: undefined,
86
+ history: [],
87
+ permissionPolicy: isPermissionPolicyValue(String(options.defaultPermissionPolicy))
88
+ ? options.defaultPermissionPolicy
89
+ : 'ask',
90
+ modeId: options.resolveExposedAcpModeId(options.defaultModeId),
91
+ })),
92
+ deletedSessions: [],
93
+ };
94
+ }
95
+ if (raw.schemaVersion === 2) {
96
+ return {
97
+ schemaVersion: 2,
98
+ sessions: raw.sessions,
99
+ deletedSessions: pruneDeletedSessionRecords(raw.deletedSessions),
100
+ };
101
+ }
102
+ return { schemaVersion: 2, sessions: [] };
103
+ }
104
+ // -------------------------------------------------------------------------
105
+ // Public API
106
+ // -------------------------------------------------------------------------
107
+ async function hydrate() {
108
+ if (sessionsHydrated)
109
+ return;
110
+ if (hydratePromise)
111
+ return hydratePromise;
112
+ hydratePromise = (async () => {
113
+ sessionsHydrated = true;
114
+ if (!options.path)
115
+ return;
116
+ try {
117
+ const raw = await readFile(options.path, 'utf8');
118
+ const parsed = normalizePersistedSessionStore(JSON.parse(raw));
119
+ const deletedIds = new Set(parsed.deletedSessions?.map((record) => record.id) ?? []);
120
+ for (const record of parsed.deletedSessions ?? []) {
121
+ deletedSessionIds.set(record.id, record.deletedAt);
122
+ }
123
+ for (const stored of pruneSessionRecords(parsed.sessions)) {
124
+ if (deletedIds.has(stored.id))
125
+ continue;
126
+ const runtimeState = {
127
+ permissionPolicy: isPermissionPolicyValue(String(stored.permissionPolicy))
128
+ ? stored.permissionPolicy
129
+ : options.defaultPermissionPolicy,
130
+ modeId: options.resolveExposedAcpModeId(stored.modeId, options.defaultModeId),
131
+ };
132
+ options.sessions.upsert({
133
+ id: stored.id,
134
+ cwd: stored.cwd,
135
+ mcpServers: Array.isArray(stored.mcpServers) ? stored.mcpServers : [],
136
+ createdAt: stored.createdAt,
137
+ updatedAt: stored.updatedAt,
138
+ title: stored.title,
139
+ taskId: stored.taskId,
140
+ permissionPolicy: runtimeState.permissionPolicy,
141
+ modeId: runtimeState.modeId,
142
+ history: Array.isArray(stored.history)
143
+ ? stored.history.slice(-options.storePolicy.historyMaxEntries)
144
+ : [],
145
+ materialized: true,
146
+ cancelRequested: false,
147
+ });
148
+ if (!options.sessionRuntime.has(stored.id)) {
149
+ options.sessionRuntime.set(stored.id, runtimeState);
150
+ }
151
+ }
152
+ }
153
+ catch (error) {
154
+ if (isFileMissing(error))
155
+ return;
156
+ recordAuditEvent('acp.session.hydrate.failed', {
157
+ errorName: error instanceof Error ? error.name : typeof error,
158
+ }, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
159
+ }
160
+ })();
161
+ return hydratePromise;
162
+ }
163
+ async function persist() {
164
+ if (!options.path)
165
+ return;
166
+ const dir = defaultPathAdapter.dirname(options.path);
167
+ const lockPath = `${options.path}.lock`;
168
+ const baseRecords = options.sessions
169
+ .list()
170
+ .filter(options.isPersistableSession)
171
+ .map((session) => {
172
+ const runtimeState = options.ensureSessionRuntimeState(session.id);
173
+ return {
174
+ id: session.id,
175
+ cwd: session.cwd,
176
+ mcpServers: session.mcpServers,
177
+ createdAt: session.createdAt,
178
+ updatedAt: session.updatedAt,
179
+ title: session.title,
180
+ taskId: session.taskId,
181
+ history: session.history.slice(-options.storePolicy.historyMaxEntries),
182
+ permissionPolicy: session.permissionPolicy ?? runtimeState.permissionPolicy,
183
+ modeId: session.modeId ?? runtimeState.modeId,
184
+ };
185
+ });
186
+ const prunedRecords = pruneSessionRecords(baseRecords);
187
+ const keepIds = new Set(prunedRecords.map((record) => record.id));
188
+ for (const record of options.sessions.list()) {
189
+ if (options.isPersistableSession(record) && !keepIds.has(record.id)) {
190
+ options.sessions.delete(record.id);
191
+ }
192
+ }
193
+ const payload = { schemaVersion: 2, sessions: prunedRecords };
194
+ const payloadDeletedSessions = pruneDeletedSessionRecords(Array.from(deletedSessionIds, ([id, deletedAt]) => ({ id, deletedAt })));
195
+ const primaryRepoPath = prunedRecords[0]?.cwd;
196
+ const lockAuditDetails = {
197
+ lockPath,
198
+ lockPathHash: hashRepoPath(lockPath),
199
+ repoPathHash: primaryRepoPath ? hashRepoPath(primaryRepoPath) : undefined,
200
+ };
201
+ const tryClearStaleLock = async () => {
202
+ try {
203
+ const raw = await readFile(lockPath, 'utf8');
204
+ const parsed = JSON.parse(raw);
205
+ const createdAtMs = typeof parsed.createdAtMs === 'number' && Number.isFinite(parsed.createdAtMs)
206
+ ? parsed.createdAtMs
207
+ : null;
208
+ if (createdAtMs === null)
209
+ return;
210
+ if (Date.now() - createdAtMs <= options.storePolicy.lockStaleMs)
211
+ return;
212
+ if (typeof parsed.pid === 'number' && isPidAlive(parsed.pid))
213
+ return;
214
+ await unlink(lockPath);
215
+ recordAuditEvent('acp.session.lock.stale_reclaimed', lockAuditDetails, {
216
+ source: 'acp',
217
+ severity: 'low',
218
+ scope: 'session',
219
+ phase: 'PREFLIGHT',
220
+ });
221
+ }
222
+ catch {
223
+ try {
224
+ const lockStat = await stat(lockPath);
225
+ const ageMs = Date.now() - lockStat.mtimeMs;
226
+ if (Number.isFinite(ageMs) && ageMs > options.storePolicy.lockStaleMs * 2) {
227
+ await unlink(lockPath);
228
+ recordAuditEvent('acp.session.lock.corrupted_reclaimed', {
229
+ ...lockAuditDetails,
230
+ ageMs: Math.max(0, Math.floor(ageMs)),
231
+ }, { source: 'acp', severity: 'medium', scope: 'session', phase: 'PREFLIGHT' });
232
+ }
233
+ }
234
+ catch {
235
+ // ignore
236
+ }
237
+ }
238
+ };
239
+ let lockHandle;
240
+ try {
241
+ await mkdir(dir, { recursive: true });
242
+ const acquireDeadlineMs = Date.now() + Math.max(250, options.storePolicy.lockAcquireTimeoutMs);
243
+ for (let attempt = 0; Date.now() < acquireDeadlineMs; attempt += 1) {
244
+ try {
245
+ lockHandle = await open(lockPath, 'wx');
246
+ await lockHandle.writeFile(JSON.stringify({ pid: process.pid, createdAtMs: Date.now() }), 'utf8');
247
+ break;
248
+ }
249
+ catch {
250
+ await tryClearStaleLock();
251
+ const delayMs = Math.min(250, 20 * (attempt + 1));
252
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
253
+ }
254
+ }
255
+ if (!lockHandle) {
256
+ recordAuditEvent('acp.session.lock.acquire_timeout', lockAuditDetails, {
257
+ source: 'acp',
258
+ severity: 'medium',
259
+ scope: 'session',
260
+ phase: 'PREFLIGHT',
261
+ });
262
+ throw new Error('ACP_SESSION_PERSIST_LOCK_TIMEOUT');
263
+ }
264
+ const heartbeat = setInterval(() => {
265
+ void writeFile(lockPath, JSON.stringify({ pid: process.pid, createdAtMs: Date.now() }), 'utf8');
266
+ }, Math.max(1000, options.storePolicy.lockHeartbeatMs));
267
+ const tempPath = defaultPathAdapter.join(dir, `.sessions.v1.json.tmp-${process.pid}-${Date.now()}`);
268
+ try {
269
+ let existing = { schemaVersion: 2, sessions: [] };
270
+ try {
271
+ const existingRaw = await readFile(options.path, 'utf8');
272
+ existing = normalizePersistedSessionStore(JSON.parse(existingRaw));
273
+ }
274
+ catch {
275
+ // ignore read failure; writing fresh payload is acceptable
276
+ }
277
+ const merged = new Map();
278
+ const mergedDeletedSessions = pruneDeletedSessionRecords([
279
+ ...(existing.deletedSessions ?? []),
280
+ ...payloadDeletedSessions,
281
+ ]);
282
+ const mergedDeletedIds = new Set(mergedDeletedSessions.map((record) => record.id));
283
+ for (const record of mergedDeletedSessions) {
284
+ deletedSessionIds.set(record.id, record.deletedAt);
285
+ }
286
+ for (const entry of existing.sessions)
287
+ merged.set(entry.id, entry);
288
+ for (const entry of payload.sessions)
289
+ merged.set(entry.id, entry);
290
+ for (const id of mergedDeletedIds)
291
+ merged.delete(id);
292
+ const mergedPayload = {
293
+ schemaVersion: 2,
294
+ sessions: pruneSessionRecords(Array.from(merged.values())),
295
+ deletedSessions: mergedDeletedSessions,
296
+ };
297
+ await writeFile(tempPath, JSON.stringify(mergedPayload, null, 2), 'utf8');
298
+ await rename(tempPath, options.path);
299
+ }
300
+ finally {
301
+ clearInterval(heartbeat);
302
+ }
303
+ }
304
+ catch (error) {
305
+ recordAuditEvent('acp.session.persist.failed', {
306
+ errorName: error instanceof Error ? error.name : typeof error,
307
+ }, { source: 'acp', severity: 'low', scope: 'session', phase: 'PREFLIGHT' });
308
+ }
309
+ finally {
310
+ if (lockHandle) {
311
+ try {
312
+ await lockHandle.close();
313
+ }
314
+ catch {
315
+ // ignore
316
+ }
317
+ try {
318
+ await unlink(lockPath);
319
+ }
320
+ catch {
321
+ // ignore
322
+ }
323
+ }
324
+ }
325
+ }
326
+ function markDeleted(id) {
327
+ deletedSessionIds.set(id, new Date().toISOString());
328
+ }
329
+ return {
330
+ hydrate,
331
+ persist,
332
+ markDeleted,
333
+ getDeletedSessionIds: () => deletedSessionIds,
334
+ };
335
+ }
336
+ //# sourceMappingURL=acp-session-persistence.js.map
@@ -0,0 +1,17 @@
1
+ import { createHash } from 'crypto';
2
+ export const ACP_PERMISSION_POLICY_ASK = 'ask';
3
+ export const ACP_PERMISSION_POLICY_DENY_ALL = 'deny_all';
4
+ export const ACP_PERMISSION_POLICY_ALLOW_ALL = 'allow_all';
5
+ export function isPermissionPolicyValue(value) {
6
+ return value === 'ask' || value === 'deny_all' || value === 'allow_all';
7
+ }
8
+ export function parseTimestamp(value) {
9
+ if (typeof value !== 'string' || value.length === 0)
10
+ return 0;
11
+ const parsed = Date.parse(value);
12
+ return Number.isFinite(parsed) ? parsed : 0;
13
+ }
14
+ export function hashRepoPath(repoPath) {
15
+ return createHash('sha256').update(repoPath).digest('hex').slice(0, 16);
16
+ }
17
+ //# sourceMappingURL=acp-types.js.map