praxis-agent 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/application/background-agent-manager.d.ts +16 -6
- package/dist/application/background-agent-manager.js +139 -97
- package/dist/application/context-engine.d.ts +56 -0
- package/dist/application/context-engine.js +129 -0
- package/dist/application/durable-follow-up.d.ts +14 -0
- package/dist/application/durable-follow-up.js +52 -0
- package/dist/application/native-session-transcript.d.ts +72 -0
- package/dist/application/native-session-transcript.js +351 -0
- package/dist/application/native-sidechain-transcript.d.ts +34 -0
- package/dist/application/native-sidechain-transcript.js +156 -0
- package/dist/application/session-service.d.ts +23 -4
- package/dist/application/session-service.js +1130 -494
- package/dist/application/subagent-service.d.ts +5 -1
- package/dist/application/subagent-service.js +749 -320
- package/dist/application/team-agent-runtime.d.ts +29 -0
- package/dist/application/team-agent-runtime.js +169 -0
- package/dist/application/team-capability.d.ts +27 -0
- package/dist/application/team-capability.js +41 -0
- package/dist/application/team-lead-operations.d.ts +38 -0
- package/dist/application/team-lead-operations.js +155 -0
- package/dist/application/team-mailbox.d.ts +54 -0
- package/dist/application/team-mailbox.js +196 -0
- package/dist/application/team-manager.d.ts +113 -0
- package/dist/application/team-manager.js +681 -0
- package/dist/application/team-workspace.d.ts +29 -0
- package/dist/application/team-workspace.js +197 -0
- package/dist/application/top-level-agent-manager.d.ts +1 -0
- package/dist/application/top-level-agent-manager.js +1025 -403
- package/dist/application/transcript-projection.d.ts +35 -0
- package/dist/application/transcript-projection.js +139 -0
- package/dist/application/turn-lifecycle.d.ts +2 -0
- package/dist/application/turn-memory-coordinator.d.ts +43 -0
- package/dist/application/turn-memory-coordinator.js +98 -0
- package/dist/cli/agents-dashboard.js +10 -5
- package/dist/cli/interactive.d.ts +7 -0
- package/dist/cli/interactive.js +645 -334
- package/dist/cli/tui/claude-style.d.ts +8 -25
- package/dist/cli/tui/claude-style.js +299 -277
- package/dist/cli/tui/decision-surface-model.d.ts +71 -0
- package/dist/cli/tui/decision-surface-model.js +131 -0
- package/dist/cli/tui/decision-surface.d.ts +7 -0
- package/dist/cli/tui/decision-surface.js +41 -0
- package/dist/cli/tui/help-surface-model.d.ts +68 -0
- package/dist/cli/tui/help-surface-model.js +131 -0
- package/dist/cli/tui/mcp-panel.js +1 -2
- package/dist/cli/tui/permission-surface-model.d.ts +195 -0
- package/dist/cli/tui/permission-surface-model.js +360 -0
- package/dist/cli/tui/permission-surface.d.ts +7 -0
- package/dist/cli/tui/permission-surface.js +69 -0
- package/dist/cli/tui/presentation-environment.d.ts +32 -0
- package/dist/cli/tui/presentation-environment.js +149 -0
- package/dist/cli/tui/task-panel.js +10 -1
- package/dist/cli/tui/theme.d.ts +46 -3
- package/dist/cli/tui/theme.js +414 -56
- package/dist/cli/tui/tool-permission.js +16 -8
- package/dist/cli/tui/transcript-presentation.d.ts +19 -1
- package/dist/cli/tui/transcript-presentation.js +26 -6
- package/dist/cli/tui/transcript-viewport.d.ts +18 -0
- package/dist/cli/tui/transcript-viewport.js +517 -111
- package/dist/cli/tui/transcript-window-model.d.ts +108 -0
- package/dist/cli/tui/transcript-window-model.js +772 -0
- package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
- package/dist/cli/tui/tui-interaction-router.js +127 -0
- package/dist/cli/tui/tui-screen-model.d.ts +114 -0
- package/dist/cli/tui/tui-screen-model.js +210 -0
- package/dist/cli/tui/tui-view-model.d.ts +2 -49
- package/dist/cli/tui/tui-view-model.js +1 -68
- package/dist/cli-runtime.d.ts +4 -2
- package/dist/cli-runtime.js +397 -32
- package/dist/compatibility/claude/transcript-codec.js +6 -0
- package/dist/core/agent-orchestration.d.ts +31 -0
- package/dist/core/agent-orchestration.js +235 -0
- package/dist/core/permission-policy.d.ts +4 -0
- package/dist/core/permission-policy.js +21 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/core/runtime.js +20 -2
- package/dist/core/team-mailbox.d.ts +54 -0
- package/dist/core/team-mailbox.js +212 -0
- package/dist/core/team-ownership.d.ts +84 -0
- package/dist/core/team-ownership.js +505 -0
- package/dist/core/tool-execution-scheduler.js +6 -1
- package/dist/core/transcript-event.d.ts +5 -1
- package/dist/core/transcript-event.js +12 -0
- package/dist/persistence/claude-job-store.d.ts +58 -0
- package/dist/persistence/claude-job-store.js +483 -1
- package/dist/persistence/claude-transcript-store.d.ts +2 -1
- package/dist/persistence/claude-transcript-store.js +2 -10
- package/dist/persistence/native-team-mailbox.d.ts +68 -0
- package/dist/persistence/native-team-mailbox.js +542 -0
- package/dist/persistence/native-team-store.d.ts +34 -0
- package/dist/persistence/native-team-store.js +257 -0
- package/dist/persistence/native-transcript-reader.d.ts +53 -0
- package/dist/persistence/native-transcript-reader.js +283 -0
- package/dist/persistence/native-transcript-store.d.ts +69 -0
- package/dist/persistence/native-transcript-store.js +321 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
- package/dist/persistence/subagent-lifecycle-store.js +373 -146
- package/dist/persistence/transcript-file-append.d.ts +2 -0
- package/dist/persistence/transcript-file-append.js +10 -0
- package/dist/platform/exclusive-file-lease.d.ts +3 -0
- package/dist/platform/exclusive-file-lease.js +3 -0
- package/dist/tools/claude-capabilities.d.ts +4 -0
- package/dist/tools/claude-capabilities.js +18 -2
- package/dist/tools/team-lead-tools.d.ts +19 -0
- package/dist/tools/team-lead-tools.js +479 -0
- package/dist/tools/team-member-tools.d.ts +21 -0
- package/dist/tools/team-member-tools.js +259 -0
- package/package.json +1 -1
- package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
- package/dist/cli/tui/fullscreen-renderer.js +0 -13
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { chmod, readFile, open } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { parseTeamMailboxMessageId, parseTeamMailboxMessage, } from '../core/team-mailbox.js';
|
|
4
|
+
import { ExclusiveFileLease, } from '../platform/exclusive-file-lease.js';
|
|
5
|
+
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
6
|
+
import { sanitizeProjectPath } from '../platform/project-path-key.js';
|
|
7
|
+
import { parseTeamId } from '../core/team-ownership.js';
|
|
8
|
+
const DEFAULT_MAX_RECORDS = 4096;
|
|
9
|
+
const DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
11
|
+
function nonblank(value, label) {
|
|
12
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
13
|
+
throw new Error(`Invalid ${label}`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function strictParticipant(value, label) {
|
|
17
|
+
return nonblank(value, label);
|
|
18
|
+
}
|
|
19
|
+
function increment(value, label) {
|
|
20
|
+
if (!Number.isSafeInteger(value) ||
|
|
21
|
+
value < 0 ||
|
|
22
|
+
value >= Number.MAX_SAFE_INTEGER)
|
|
23
|
+
throw new Error(`${label} exhausted`);
|
|
24
|
+
return value + 1;
|
|
25
|
+
}
|
|
26
|
+
function object(value, label) {
|
|
27
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
28
|
+
throw new Error(`Invalid ${label}`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function closed(value, keys) {
|
|
32
|
+
const allowed = new Set(keys);
|
|
33
|
+
for (const key of Object.keys(value))
|
|
34
|
+
if (!allowed.has(key))
|
|
35
|
+
throw new Error(`Unknown mailbox state field: ${key}`);
|
|
36
|
+
}
|
|
37
|
+
function iso(value) {
|
|
38
|
+
if (typeof value !== 'string' ||
|
|
39
|
+
!ISO.test(value) ||
|
|
40
|
+
Number.isNaN(Date.parse(value)) ||
|
|
41
|
+
new Date(value).toISOString() !== value)
|
|
42
|
+
throw new Error('Invalid mailbox state timestamp');
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function now() {
|
|
46
|
+
return new Date().toISOString();
|
|
47
|
+
}
|
|
48
|
+
function serialized(value) {
|
|
49
|
+
return `${JSON.stringify(value)}\n`;
|
|
50
|
+
}
|
|
51
|
+
function sameContent(left, right) {
|
|
52
|
+
return (left.teamId === right.teamId &&
|
|
53
|
+
left.sender === right.sender &&
|
|
54
|
+
JSON.stringify(left.recipients) === JSON.stringify(right.recipients) &&
|
|
55
|
+
JSON.stringify(left.payload) === JSON.stringify(right.payload));
|
|
56
|
+
}
|
|
57
|
+
export class NativeTeamMailbox {
|
|
58
|
+
options;
|
|
59
|
+
messagesPath;
|
|
60
|
+
statePath;
|
|
61
|
+
mutationPath;
|
|
62
|
+
participants;
|
|
63
|
+
participantSet;
|
|
64
|
+
maxRecords;
|
|
65
|
+
maxBytes;
|
|
66
|
+
mutation = Promise.resolve();
|
|
67
|
+
constructor(options) {
|
|
68
|
+
this.options = options;
|
|
69
|
+
const root = resolve(options.nativeRoot);
|
|
70
|
+
const directory = resolve(root, 'state', 'teams', sanitizeProjectPath(options.projectIdentity), options.teamId, 'mailbox');
|
|
71
|
+
this.messagesPath = resolve(directory, 'messages.jsonl');
|
|
72
|
+
this.statePath = resolve(directory, 'state.json');
|
|
73
|
+
this.mutationPath = resolve(directory, 'mutation.lock');
|
|
74
|
+
this.participants = Object.freeze([...options.participants]);
|
|
75
|
+
this.participantSet = new Set(this.participants);
|
|
76
|
+
this.maxRecords = options.retention?.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
77
|
+
this.maxBytes = options.retention?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
78
|
+
}
|
|
79
|
+
static open(options) {
|
|
80
|
+
if (typeof options.nativeRoot !== 'string' ||
|
|
81
|
+
options.nativeRoot.trim() === '')
|
|
82
|
+
throw new Error('Invalid native mailbox root');
|
|
83
|
+
const projectIdentity = nonblank(options.projectIdentity, 'project identity');
|
|
84
|
+
const teamId = parseTeamId(options.teamId);
|
|
85
|
+
if (!Array.isArray(options.participants) ||
|
|
86
|
+
options.participants.length === 0)
|
|
87
|
+
throw new Error('Invalid mailbox participants');
|
|
88
|
+
const participants = options.participants.map((entry) => strictParticipant(entry, 'participant'));
|
|
89
|
+
if (new Set(participants).size !== participants.length)
|
|
90
|
+
throw new Error('Duplicate mailbox participant');
|
|
91
|
+
if (!participants.includes('lead'))
|
|
92
|
+
throw new Error('Mailbox participants must include lead');
|
|
93
|
+
const maxRecords = options.retention?.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
94
|
+
const maxBytes = options.retention?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
95
|
+
if (!Number.isSafeInteger(maxRecords) || maxRecords <= 0)
|
|
96
|
+
throw new Error('Invalid mailbox maxRecords');
|
|
97
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
98
|
+
throw new Error('Invalid mailbox maxBytes');
|
|
99
|
+
return new NativeTeamMailbox({
|
|
100
|
+
...options,
|
|
101
|
+
projectIdentity,
|
|
102
|
+
teamId,
|
|
103
|
+
participants,
|
|
104
|
+
retention: { maxRecords, maxBytes },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
append(input) {
|
|
108
|
+
return this.enqueue(() => this.mutate(async (state, records) => {
|
|
109
|
+
const candidate = parseTeamMailboxMessage({
|
|
110
|
+
version: 1,
|
|
111
|
+
sequence: state.nextSequence,
|
|
112
|
+
messageId: input.messageId,
|
|
113
|
+
teamId: this.options.teamId,
|
|
114
|
+
sender: input.sender,
|
|
115
|
+
recipients: input.recipients,
|
|
116
|
+
payload: input.payload,
|
|
117
|
+
createdAt: input.createdAt ?? now(),
|
|
118
|
+
});
|
|
119
|
+
this.assertParticipants(candidate);
|
|
120
|
+
const duplicate = records.find((record) => record.messageId === candidate.messageId);
|
|
121
|
+
if (duplicate) {
|
|
122
|
+
if (!sameContent(duplicate, candidate))
|
|
123
|
+
throw new Error('Mailbox message ID conflicts with existing content');
|
|
124
|
+
return {
|
|
125
|
+
result: { message: duplicate, inserted: false },
|
|
126
|
+
state,
|
|
127
|
+
records,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
let bytes = records.reduce((total, record) => total + Buffer.byteLength(serialized(record), 'utf8'), 0);
|
|
131
|
+
const appendedBytes = Buffer.byteLength(serialized(candidate), 'utf8');
|
|
132
|
+
if (records.length + 1 > this.maxRecords ||
|
|
133
|
+
bytes + appendedBytes > this.maxBytes) {
|
|
134
|
+
const compacted = await this.compact(state, records);
|
|
135
|
+
state = compacted.state;
|
|
136
|
+
records = compacted.records;
|
|
137
|
+
bytes = records.reduce((total, record) => total + Buffer.byteLength(serialized(record), 'utf8'), 0);
|
|
138
|
+
}
|
|
139
|
+
if (records.length + 1 > this.maxRecords ||
|
|
140
|
+
bytes + appendedBytes > this.maxBytes)
|
|
141
|
+
throw new Error('Team mailbox backpressure: unread messages exceed retention bounds');
|
|
142
|
+
const nextSequence = increment(candidate.sequence, 'Mailbox sequence');
|
|
143
|
+
const nextRevision = increment(state.revision, 'Mailbox revision');
|
|
144
|
+
const line = serialized(candidate);
|
|
145
|
+
const handle = await open(this.messagesPath, 'a', 0o600);
|
|
146
|
+
let offset = 0;
|
|
147
|
+
try {
|
|
148
|
+
await chmod(this.messagesPath, 0o600);
|
|
149
|
+
const stat = await handle.stat();
|
|
150
|
+
offset = stat.size;
|
|
151
|
+
await handle.writeFile(line, 'utf8');
|
|
152
|
+
await handle.sync();
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await handle.close();
|
|
156
|
+
}
|
|
157
|
+
const exact = await readFile(this.messagesPath);
|
|
158
|
+
const expected = Buffer.from(line, 'utf8');
|
|
159
|
+
if (!exact.subarray(offset).equals(expected))
|
|
160
|
+
throw new Error('Mailbox append verification failed');
|
|
161
|
+
const nextState = {
|
|
162
|
+
...state,
|
|
163
|
+
revision: nextRevision,
|
|
164
|
+
nextSequence,
|
|
165
|
+
updatedAt: now(),
|
|
166
|
+
};
|
|
167
|
+
await this.writeState(nextState);
|
|
168
|
+
return {
|
|
169
|
+
result: { message: candidate, inserted: true },
|
|
170
|
+
state: nextState,
|
|
171
|
+
records: [...records, candidate],
|
|
172
|
+
};
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
read(participant, bounds = {}) {
|
|
176
|
+
return this.enqueue(() => this.mutate(async (state, records) => {
|
|
177
|
+
this.assertParticipant(participant);
|
|
178
|
+
const expectedCursor = state.cursors[participant] ?? 0;
|
|
179
|
+
const maxMessages = bounds.maxMessages ?? Number.MAX_SAFE_INTEGER;
|
|
180
|
+
if (!Number.isSafeInteger(maxMessages) || maxMessages <= 0)
|
|
181
|
+
throw new Error('Invalid mailbox read maxMessages');
|
|
182
|
+
const messages = records
|
|
183
|
+
.filter((record) => record.sequence > expectedCursor &&
|
|
184
|
+
record.recipients.includes(participant))
|
|
185
|
+
.slice(0, maxMessages);
|
|
186
|
+
const throughSequence = messages.at(-1)?.sequence ?? expectedCursor;
|
|
187
|
+
const messageIds = messages.map((message) => message.messageId);
|
|
188
|
+
return {
|
|
189
|
+
result: {
|
|
190
|
+
messages: Object.freeze(messages),
|
|
191
|
+
expectedCursor,
|
|
192
|
+
throughSequence,
|
|
193
|
+
messageIds: Object.freeze(messageIds),
|
|
194
|
+
proof: Object.freeze({
|
|
195
|
+
participant,
|
|
196
|
+
expectedCursor,
|
|
197
|
+
throughSequence,
|
|
198
|
+
messageIds: Object.freeze(messageIds),
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
state,
|
|
202
|
+
records,
|
|
203
|
+
};
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
acknowledge(proof) {
|
|
207
|
+
return this.enqueue(() => this.mutate(async (state, records) => {
|
|
208
|
+
this.assertParticipant(proof.participant);
|
|
209
|
+
if (!Number.isSafeInteger(proof.expectedCursor) ||
|
|
210
|
+
proof.expectedCursor < 0 ||
|
|
211
|
+
!Number.isSafeInteger(proof.throughSequence) ||
|
|
212
|
+
proof.throughSequence < proof.expectedCursor ||
|
|
213
|
+
!Array.isArray(proof.messageIds) ||
|
|
214
|
+
proof.messageIds.some((messageId) => {
|
|
215
|
+
try {
|
|
216
|
+
parseTeamMailboxMessageId(messageId);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}))
|
|
223
|
+
throw new Error('Invalid mailbox acknowledgement proof');
|
|
224
|
+
const current = state.cursors[proof.participant] ?? 0;
|
|
225
|
+
if (proof.throughSequence >= state.nextSequence)
|
|
226
|
+
throw new Error('Mailbox acknowledgement exceeds durable high-watermark');
|
|
227
|
+
const targeted = records.filter((record) => record.sequence > proof.expectedCursor &&
|
|
228
|
+
record.sequence <= proof.throughSequence &&
|
|
229
|
+
record.recipients.includes(proof.participant));
|
|
230
|
+
const matches = targeted.length === proof.messageIds.length &&
|
|
231
|
+
targeted.every((record, index) => record.messageId === proof.messageIds[index]);
|
|
232
|
+
if (proof.expectedCursor < current) {
|
|
233
|
+
if (proof.throughSequence > current)
|
|
234
|
+
throw new Error('Stale mailbox acknowledgement proof');
|
|
235
|
+
if (proof.expectedCursor < state.prunedThrough || !matches)
|
|
236
|
+
throw new Error('Mailbox acknowledgement proof is outside the retained window');
|
|
237
|
+
return { result: undefined, state, records };
|
|
238
|
+
}
|
|
239
|
+
if (proof.expectedCursor > current)
|
|
240
|
+
throw new Error('Mailbox acknowledgement cursor skipped');
|
|
241
|
+
if (!matches)
|
|
242
|
+
throw new Error('Mailbox acknowledgement proof does not match unread messages');
|
|
243
|
+
const nextCursors = {
|
|
244
|
+
...state.cursors,
|
|
245
|
+
[proof.participant]: proof.throughSequence,
|
|
246
|
+
};
|
|
247
|
+
const nextRevision = increment(state.revision, 'Mailbox revision');
|
|
248
|
+
const nextState = {
|
|
249
|
+
...state,
|
|
250
|
+
revision: nextRevision,
|
|
251
|
+
cursors: nextCursors,
|
|
252
|
+
updatedAt: now(),
|
|
253
|
+
};
|
|
254
|
+
await this.writeState(nextState);
|
|
255
|
+
return { result: undefined, state: nextState, records };
|
|
256
|
+
})).then(() => undefined);
|
|
257
|
+
}
|
|
258
|
+
enqueue(operation) {
|
|
259
|
+
const result = this.mutation.then(operation, operation);
|
|
260
|
+
this.mutation = result.then(() => undefined, () => undefined);
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
async mutate(operation) {
|
|
264
|
+
const lease = await this.acquireMutation();
|
|
265
|
+
try {
|
|
266
|
+
const { state, records } = await this.load();
|
|
267
|
+
const output = await operation(state, records);
|
|
268
|
+
return output.result;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
await lease.release();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async acquireMutation() {
|
|
275
|
+
const deadline = Date.now() + 5000;
|
|
276
|
+
for (;;) {
|
|
277
|
+
const lease = await new ExclusiveFileLease(this.mutationPath).tryAcquire();
|
|
278
|
+
if (lease)
|
|
279
|
+
return lease;
|
|
280
|
+
if (Date.now() >= deadline)
|
|
281
|
+
throw new Error(`Team mailbox mutation is busy: ${this.mutationPath}`);
|
|
282
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 10));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async load() {
|
|
286
|
+
const state = await this.loadState();
|
|
287
|
+
await this.ensureMode(this.statePath);
|
|
288
|
+
await this.ensureMode(this.messagesPath);
|
|
289
|
+
let source = '';
|
|
290
|
+
try {
|
|
291
|
+
source = await readFile(this.messagesPath, 'utf8');
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error.code !== 'ENOENT')
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
if (source && !source.endsWith('\n')) {
|
|
298
|
+
const lastNewline = source.lastIndexOf('\n');
|
|
299
|
+
const committedBytes = lastNewline < 0
|
|
300
|
+
? 0
|
|
301
|
+
: Buffer.byteLength(source.slice(0, lastNewline + 1), 'utf8');
|
|
302
|
+
await this.truncateAndSync(committedBytes);
|
|
303
|
+
source = lastNewline < 0 ? '' : source.slice(0, lastNewline + 1);
|
|
304
|
+
}
|
|
305
|
+
const records = [];
|
|
306
|
+
let previous;
|
|
307
|
+
let retainedStarted = false;
|
|
308
|
+
let prunedRows = 0;
|
|
309
|
+
const ids = new Set();
|
|
310
|
+
const lines = source.split('\n');
|
|
311
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
312
|
+
const line = lines[index];
|
|
313
|
+
if (line === undefined)
|
|
314
|
+
throw new Error('Invalid mailbox record line');
|
|
315
|
+
if (!line) {
|
|
316
|
+
if (index === lines.length - 1)
|
|
317
|
+
continue;
|
|
318
|
+
throw new Error('Invalid blank Team mailbox record');
|
|
319
|
+
}
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = JSON.parse(line);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
throw new Error('Corrupt Team mailbox record', { cause: error });
|
|
326
|
+
}
|
|
327
|
+
const message = parseTeamMailboxMessage(parsed);
|
|
328
|
+
this.assertParticipants(message, state);
|
|
329
|
+
if (ids.has(message.messageId))
|
|
330
|
+
throw new Error('Duplicate Team mailbox message ID');
|
|
331
|
+
ids.add(message.messageId);
|
|
332
|
+
if (previous !== undefined &&
|
|
333
|
+
message.sequence !== increment(previous, 'Mailbox sequence'))
|
|
334
|
+
throw new Error('Team mailbox physical records are not contiguous');
|
|
335
|
+
if (message.sequence <= state.prunedThrough) {
|
|
336
|
+
if (retainedStarted)
|
|
337
|
+
throw new Error('Invalid pruned Team mailbox prefix');
|
|
338
|
+
previous = message.sequence;
|
|
339
|
+
prunedRows += 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const expected = increment(state.prunedThrough, 'Mailbox sequence');
|
|
343
|
+
retainedStarted = true;
|
|
344
|
+
if (records.length === 0 && message.sequence !== expected)
|
|
345
|
+
throw new Error('Team mailbox records are not contiguous');
|
|
346
|
+
records.push(message);
|
|
347
|
+
previous = message.sequence;
|
|
348
|
+
}
|
|
349
|
+
if (prunedRows > 0 && !retainedStarted && previous !== state.prunedThrough)
|
|
350
|
+
throw new Error('Incomplete pruned Team mailbox prefix');
|
|
351
|
+
const expectedNext = retainedStarted
|
|
352
|
+
? increment(previous, 'Mailbox sequence')
|
|
353
|
+
: increment(state.prunedThrough, 'Mailbox sequence');
|
|
354
|
+
if (state.nextSequence > expectedNext)
|
|
355
|
+
throw new Error('Mailbox state high-watermark is not durable');
|
|
356
|
+
if (state.nextSequence < expectedNext) {
|
|
357
|
+
const nextRevision = increment(state.revision, 'Mailbox revision');
|
|
358
|
+
const nextState = {
|
|
359
|
+
...state,
|
|
360
|
+
nextSequence: expectedNext,
|
|
361
|
+
revision: nextRevision,
|
|
362
|
+
updatedAt: now(),
|
|
363
|
+
};
|
|
364
|
+
await this.writeState(nextState);
|
|
365
|
+
if (prunedRows > 0)
|
|
366
|
+
await this.rewriteRecords(records);
|
|
367
|
+
return { state: nextState, records };
|
|
368
|
+
}
|
|
369
|
+
if (prunedRows > 0)
|
|
370
|
+
await this.rewriteRecords(records);
|
|
371
|
+
return { state, records };
|
|
372
|
+
}
|
|
373
|
+
async truncateAndSync(bytes) {
|
|
374
|
+
const handle = await open(this.messagesPath, 'r+');
|
|
375
|
+
try {
|
|
376
|
+
await handle.truncate(bytes);
|
|
377
|
+
await handle.sync();
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
await handle.close();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async rewriteRecords(records) {
|
|
384
|
+
await writeFileAtomically(this.messagesPath, records.map(serialized).join(''), { mode: 0o600 });
|
|
385
|
+
}
|
|
386
|
+
async ensureMode(path) {
|
|
387
|
+
try {
|
|
388
|
+
await chmod(path, 0o600);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (error.code !== 'ENOENT')
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async loadState() {
|
|
396
|
+
let source;
|
|
397
|
+
try {
|
|
398
|
+
source = await readFile(this.statePath, 'utf8');
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
if (error.code !== 'ENOENT')
|
|
402
|
+
throw error;
|
|
403
|
+
const state = this.initialState();
|
|
404
|
+
await this.writeState(state);
|
|
405
|
+
return state;
|
|
406
|
+
}
|
|
407
|
+
let value;
|
|
408
|
+
try {
|
|
409
|
+
value = JSON.parse(source);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
throw new Error('Corrupt Team mailbox state', { cause: error });
|
|
413
|
+
}
|
|
414
|
+
const sourceObject = object(value, 'mailbox state');
|
|
415
|
+
closed(sourceObject, [
|
|
416
|
+
'version',
|
|
417
|
+
'revision',
|
|
418
|
+
'teamId',
|
|
419
|
+
'projectIdentity',
|
|
420
|
+
'nextSequence',
|
|
421
|
+
'prunedThrough',
|
|
422
|
+
'cursors',
|
|
423
|
+
'updatedAt',
|
|
424
|
+
]);
|
|
425
|
+
if (sourceObject.version !== 1 ||
|
|
426
|
+
!Number.isSafeInteger(sourceObject.revision) ||
|
|
427
|
+
sourceObject.revision < 0 ||
|
|
428
|
+
!Number.isSafeInteger(sourceObject.nextSequence) ||
|
|
429
|
+
sourceObject.nextSequence <= 0 ||
|
|
430
|
+
!Number.isSafeInteger(sourceObject.prunedThrough) ||
|
|
431
|
+
sourceObject.prunedThrough < 0 ||
|
|
432
|
+
sourceObject.nextSequence <=
|
|
433
|
+
sourceObject.prunedThrough)
|
|
434
|
+
throw new Error('Invalid Team mailbox state counters');
|
|
435
|
+
if (sourceObject.teamId !== this.options.teamId ||
|
|
436
|
+
sourceObject.projectIdentity !== this.options.projectIdentity)
|
|
437
|
+
throw new Error('Invalid Team mailbox state identity');
|
|
438
|
+
const cursorsObject = object(sourceObject.cursors, 'mailbox cursors');
|
|
439
|
+
const cursorParticipants = Object.keys(cursorsObject);
|
|
440
|
+
if (cursorParticipants.some((key) => key.trim() === ''))
|
|
441
|
+
throw new Error('Invalid mailbox cursor participant');
|
|
442
|
+
const cursors = Object.create(null);
|
|
443
|
+
let addedCurrentCursor = false;
|
|
444
|
+
for (const historical of cursorParticipants) {
|
|
445
|
+
const cursor = cursorsObject[historical];
|
|
446
|
+
if (!Number.isSafeInteger(cursor) ||
|
|
447
|
+
cursor < 0 ||
|
|
448
|
+
cursor > sourceObject.nextSequence - 1)
|
|
449
|
+
throw new Error('Invalid mailbox cursor');
|
|
450
|
+
cursors[historical] = cursor;
|
|
451
|
+
}
|
|
452
|
+
for (const participant of this.participants) {
|
|
453
|
+
if (!Object.hasOwn(cursorsObject, participant)) {
|
|
454
|
+
cursors[participant] = sourceObject.prunedThrough;
|
|
455
|
+
addedCurrentCursor = true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const parsedState = {
|
|
459
|
+
version: 1,
|
|
460
|
+
revision: sourceObject.revision,
|
|
461
|
+
teamId: this.options.teamId,
|
|
462
|
+
projectIdentity: this.options.projectIdentity,
|
|
463
|
+
nextSequence: sourceObject.nextSequence,
|
|
464
|
+
prunedThrough: sourceObject.prunedThrough,
|
|
465
|
+
cursors,
|
|
466
|
+
updatedAt: iso(sourceObject.updatedAt),
|
|
467
|
+
};
|
|
468
|
+
if (!addedCurrentCursor)
|
|
469
|
+
return parsedState;
|
|
470
|
+
const reconciled = {
|
|
471
|
+
...parsedState,
|
|
472
|
+
revision: increment(parsedState.revision, 'Mailbox revision'),
|
|
473
|
+
updatedAt: now(),
|
|
474
|
+
};
|
|
475
|
+
await this.writeState(reconciled);
|
|
476
|
+
return reconciled;
|
|
477
|
+
}
|
|
478
|
+
initialState() {
|
|
479
|
+
return {
|
|
480
|
+
version: 1,
|
|
481
|
+
revision: 0,
|
|
482
|
+
teamId: this.options.teamId,
|
|
483
|
+
projectIdentity: this.options.projectIdentity,
|
|
484
|
+
nextSequence: 1,
|
|
485
|
+
prunedThrough: 0,
|
|
486
|
+
cursors: Object.fromEntries(this.participants.map((participant) => [participant, 0])),
|
|
487
|
+
updatedAt: now(),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
async writeState(state) {
|
|
491
|
+
await writeFileAtomically(this.statePath, serialized(state), {
|
|
492
|
+
mode: 0o600,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
async compact(state, records) {
|
|
496
|
+
let count = 0;
|
|
497
|
+
let bytes = 0;
|
|
498
|
+
for (const record of records) {
|
|
499
|
+
if (!record.recipients.every((participant) => (state.cursors[participant] ?? 0) >= record.sequence))
|
|
500
|
+
break;
|
|
501
|
+
count += 1;
|
|
502
|
+
bytes += Buffer.byteLength(serialized(record), 'utf8');
|
|
503
|
+
}
|
|
504
|
+
if (count === 0)
|
|
505
|
+
return { state, records };
|
|
506
|
+
const last = records[count - 1];
|
|
507
|
+
if (!last)
|
|
508
|
+
return { state, records };
|
|
509
|
+
const nextState = {
|
|
510
|
+
...state,
|
|
511
|
+
revision: increment(state.revision, 'Mailbox revision'),
|
|
512
|
+
prunedThrough: last.sequence,
|
|
513
|
+
updatedAt: now(),
|
|
514
|
+
};
|
|
515
|
+
await this.writeState(nextState);
|
|
516
|
+
await writeFileAtomically(this.messagesPath, records.slice(count).map(serialized).join(''), { mode: 0o600 });
|
|
517
|
+
void bytes;
|
|
518
|
+
return { state: nextState, records: records.slice(count) };
|
|
519
|
+
}
|
|
520
|
+
assertParticipant(participant) {
|
|
521
|
+
const value = strictParticipant(participant, 'participant');
|
|
522
|
+
if (!this.participantSet.has(value))
|
|
523
|
+
throw new Error(`Unknown mailbox participant: ${value}`);
|
|
524
|
+
}
|
|
525
|
+
assertParticipants(message, state) {
|
|
526
|
+
const known = state
|
|
527
|
+
? new Set(Object.keys(state.cursors))
|
|
528
|
+
: this.participantSet;
|
|
529
|
+
const assertKnown = (value) => {
|
|
530
|
+
if (!known.has(value))
|
|
531
|
+
throw new Error(`Unknown mailbox participant: ${value}`);
|
|
532
|
+
};
|
|
533
|
+
assertKnown(message.sender);
|
|
534
|
+
for (const recipient of message.recipients)
|
|
535
|
+
assertKnown(recipient);
|
|
536
|
+
if (message.recipients.includes(message.sender))
|
|
537
|
+
throw new Error('Mailbox sender cannot be a recipient');
|
|
538
|
+
if (message.teamId !== this.options.teamId)
|
|
539
|
+
throw new Error('Mailbox Team identity mismatch');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
//# sourceMappingURL=native-team-mailbox.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type TeamSnapshot } from '../core/team-ownership.js';
|
|
2
|
+
export interface NativeTeamStoreOptions {
|
|
3
|
+
readonly nativeRoot: string;
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
}
|
|
6
|
+
export interface NativeTeamClaim {
|
|
7
|
+
readonly teamId: string;
|
|
8
|
+
readonly token: string;
|
|
9
|
+
readonly pid: number;
|
|
10
|
+
readonly acquiredAt: string;
|
|
11
|
+
read(): Promise<TeamSnapshot>;
|
|
12
|
+
save(expectedRevision: number, next: TeamSnapshot): Promise<TeamSnapshot>;
|
|
13
|
+
release(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare class NativeTeamStore {
|
|
16
|
+
private readonly nativeRoot;
|
|
17
|
+
readonly projectIdentity: string;
|
|
18
|
+
private readonly projectRoot;
|
|
19
|
+
private mutation;
|
|
20
|
+
private constructor();
|
|
21
|
+
static open(options: NativeTeamStoreOptions): Promise<NativeTeamStore>;
|
|
22
|
+
createAndClaim(snapshot: unknown): Promise<NativeTeamClaim>;
|
|
23
|
+
claim(teamId: string): Promise<NativeTeamClaim>;
|
|
24
|
+
read(teamId: string): Promise<TeamSnapshot | null>;
|
|
25
|
+
list(): Promise<readonly TeamSnapshot[]>;
|
|
26
|
+
private acquireClaim;
|
|
27
|
+
private acquireMutation;
|
|
28
|
+
private enqueue;
|
|
29
|
+
private save;
|
|
30
|
+
private assertSnapshot;
|
|
31
|
+
private statePath;
|
|
32
|
+
private exists;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=native-team-store.d.ts.map
|