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.
- package/dist/cli/authorization/non-interactive.js +7 -21
- package/dist/cli/commands/chat.js +1 -1
- package/dist/cli/commands/parallel.js +46 -41
- package/dist/cli/commands/run/assistant-message.js +3 -0
- package/dist/cli/commands/run/handler.js +2 -1
- package/dist/cli/commands/serve.js +112 -156
- package/dist/cli/headless/json-protocol.js +1 -1
- package/dist/cli/headless/stream-json-protocol.js +3 -2
- package/dist/cli/program-bootstrap.js +2 -2
- package/dist/cli/slash/runtime.js +5 -1
- package/dist/core/adapters/fs/node-fs.js +1 -0
- package/dist/core/backends/salmon-loop/task-executor.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +1 -1
- package/dist/core/context/service.js +5 -2
- package/dist/core/extensions/index.js +2 -35
- package/dist/core/extensions/merge.js +14 -0
- package/dist/core/extensions/redact.js +9 -3
- package/dist/core/extensions/schemas.js +2 -51
- package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
- package/dist/core/facades/cli-program-bootstrap.js +1 -0
- package/dist/core/facades/cli-serve.js +2 -1
- package/dist/core/grizzco/dsl/strategies.js +1 -3
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
- package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
- package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
- package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
- package/dist/core/grizzco/steps/apply.js +0 -7
- package/dist/core/grizzco/steps/autopilot.js +108 -6
- package/dist/core/grizzco/steps/preflight.js +10 -0
- package/dist/core/grizzco/steps/tool-runtime.js +1 -0
- package/dist/core/interaction/events/bus.js +14 -0
- package/dist/core/interaction/orchestration/facade.js +11 -1
- package/dist/core/llm/ai-sdk/request-params.js +40 -1
- package/dist/core/mcp/bridge/index.js +4 -0
- package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
- package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
- package/dist/core/mcp/bridge/tool-bridge.js +303 -0
- package/dist/core/mcp/cache/resource-cache.js +41 -0
- package/dist/core/mcp/catalog/discovery.js +51 -0
- package/dist/core/mcp/catalog/notification-router.js +28 -0
- package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
- package/dist/core/mcp/catalog/resource-catalog.js +7 -0
- package/dist/core/mcp/catalog/tool-catalog.js +4 -0
- package/dist/core/mcp/client/connection-manager.js +239 -0
- package/dist/core/mcp/client/lifecycle.js +13 -0
- package/dist/core/mcp/client/transport-factory.js +168 -0
- package/dist/core/mcp/config/index.js +32 -0
- package/dist/core/mcp/config/schema-v2.js +129 -0
- package/dist/core/mcp/host/elicitation-provider.js +209 -0
- package/dist/core/mcp/host/roots-provider.js +70 -0
- package/dist/core/mcp/host/sampling-provider.js +170 -0
- package/dist/core/mcp/index.js +4 -0
- package/dist/core/mcp/observability/events.js +19 -0
- package/dist/core/mcp/policy/approval-policy.js +2 -0
- package/dist/core/mcp/policy/classifier.js +172 -0
- package/dist/core/mcp/policy/grants.js +356 -0
- package/dist/core/mcp/policy/uri-policy.js +60 -0
- package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
- package/dist/core/mcp/types.js +2 -0
- package/dist/core/protocols/a2a/agent-card.js +38 -12
- package/dist/core/protocols/a2a/sdk/executor.js +105 -36
- package/dist/core/protocols/a2a/sdk/server.js +1311 -3
- package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
- package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
- package/dist/core/protocols/acp/acp-types.js +17 -0
- package/dist/core/protocols/acp/formal-agent.js +389 -502
- package/dist/core/protocols/acp/handlers.js +3 -0
- package/dist/core/protocols/acp/permission-provider.js +11 -39
- package/dist/core/protocols/acp/stdio-server.js +20 -1
- package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
- package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
- package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
- package/dist/core/public-capabilities/projections.js +1 -0
- package/dist/core/runtime/agent-server-runtime.js +2 -3
- package/dist/core/runtime/spawn-command.js +8 -2
- package/dist/core/runtime/spawn-interactive.js +26 -0
- package/dist/core/session/manager.js +48 -25
- package/dist/core/tools/builtin/index.js +6 -1
- package/dist/core/tools/builtin/proposal.js +0 -7
- package/dist/core/tools/builtin/workspace.js +76 -0
- package/dist/core/tools/dispatcher.js +1 -0
- package/dist/core/tools/loader.js +92 -46
- package/dist/core/verification/runner.js +60 -31
- package/dist/core/version.js +17 -0
- package/dist/core/workspace/capabilities.js +80 -0
- package/dist/locales/en.js +17 -3
- package/package.json +4 -2
- package/dist/core/protocols/a2a/mapper.js +0 -14
- package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
- package/dist/core/protocols/a2a/task-projection.js +0 -45
- package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
- package/dist/core/tools/mcp/client.js +0 -308
- package/dist/core/tools/mcp/loader.js +0 -110
- package/dist/core/tools/mcp/schema.js +0 -54
- package/dist/core/tools/mcp/streamable-http.js +0 -101
- 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
|