clay-server 4.0.0-beta.16 → 4.0.0-beta.17
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/lib/daemon-projects.js +10 -5
- package/lib/daemon.js +61 -10
- package/lib/project-log-context.js +171 -0
- package/lib/project-log-feedback-delivery.js +47 -5
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +5 -0
- package/lib/project-logs-query.js +18 -0
- package/lib/project-logs-root.js +8 -0
- package/lib/project-logs-service.js +92 -15
- package/lib/project-logs-snapshot.js +3 -0
- package/lib/project-logs-store.js +29 -32
- package/lib/project-logs-versioning.js +6 -2
- package/lib/project-logs.js +56 -12
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-user-message.js +11 -0
- package/lib/project.js +5 -0
- package/lib/public/app.js +5 -0
- package/lib/public/css/project-logs.css +20 -2
- package/lib/public/modules/app-connection.js +22 -3
- package/lib/public/modules/app-messages.js +29 -1
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/input.js +6 -1
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/project-logs-render.js +48 -2
- package/lib/public/modules/project-logs.js +133 -11
- package/lib/server.js +6 -0
- package/lib/ws-schema.js +6 -2
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// - A project session is bound to exactly its own project's Logs.
|
|
14
14
|
// - Canonical authorship belongs to Project Driver sessions alone. A human,
|
|
15
15
|
// however privileged, may read and comment but may never create or revise
|
|
16
|
-
// a canonical entry
|
|
16
|
+
// a canonical entry. The project owner may delete an entry.
|
|
17
17
|
// - Members of a shared project read each other's entries and comments with
|
|
18
18
|
// attribution.
|
|
19
19
|
// - A worktree resolves to its parent project for both authorization and
|
|
@@ -39,6 +39,10 @@ function driverOnly() {
|
|
|
39
39
|
throw new Error("Only this project's agent sessions can review feedback or change revisions.");
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function ownerOnly() {
|
|
43
|
+
throw new Error("Only the project owner can delete Project Logs.");
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
// Bounded discovery of comments still waiting on the Driver. The store owns the
|
|
43
47
|
// one-pass scan over every live entry, so nothing here imposes a ceiling that
|
|
44
48
|
// could hide a pending comment on an older record.
|
|
@@ -51,11 +55,17 @@ function attachProjectLogsService(ctx) {
|
|
|
51
55
|
var findUserById = context.findUserById || function () { return null; };
|
|
52
56
|
var storeCache = new Map();
|
|
53
57
|
|
|
54
|
-
var openStore = context.openStore || function (cwd) {
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
var openStore = context.openStore || function (cwd, storeOptions) {
|
|
59
|
+
var options = storeOptions || {};
|
|
60
|
+
var key = options.projectKnowledgeId || cwd;
|
|
61
|
+
if (!storeCache.has(key)) {
|
|
62
|
+
storeCache.set(key, logsStore.createProjectLogsStore({
|
|
63
|
+
cwd: cwd,
|
|
64
|
+
baseDir: context.baseDir,
|
|
65
|
+
projectKnowledgeId: options.projectKnowledgeId || null,
|
|
66
|
+
}));
|
|
57
67
|
}
|
|
58
|
-
return storeCache.get(
|
|
68
|
+
return storeCache.get(key);
|
|
59
69
|
};
|
|
60
70
|
|
|
61
71
|
function projectFor(slug) {
|
|
@@ -98,7 +108,26 @@ function attachProjectLogsService(ctx) {
|
|
|
98
108
|
function storeForStatus(status) {
|
|
99
109
|
var governing = effectiveStatus(status);
|
|
100
110
|
if (!governing || !governing.path) throw new Error("Project Logs are unavailable for this project.");
|
|
101
|
-
return openStore(governing.path);
|
|
111
|
+
return openStore(governing.path, { projectKnowledgeId: governing.projectKnowledgeId || null });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function recordContext(status) {
|
|
115
|
+
if (!status || status.isWorktree !== true || !status.changeSetId) return null;
|
|
116
|
+
return {
|
|
117
|
+
kind: "worktree",
|
|
118
|
+
changeSetId: status.changeSetId,
|
|
119
|
+
branch: status.branch || null,
|
|
120
|
+
baseCommit: status.baseCommit || null,
|
|
121
|
+
headCommit: status.headCommit || null,
|
|
122
|
+
status: "active",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function scopedArgs(args, status) {
|
|
127
|
+
var out = Object.assign({}, args || {});
|
|
128
|
+
if (!out.contextMode) out.contextMode = status && status.isWorktree ? "current" : "project";
|
|
129
|
+
out.currentChangeSetId = status && status.changeSetId || null;
|
|
130
|
+
return out;
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
function statusFor(principal, slug) {
|
|
@@ -205,6 +234,7 @@ function attachProjectLogsService(ctx) {
|
|
|
205
234
|
principal.kind = "user";
|
|
206
235
|
principal.projectSlug = source.projectSlug;
|
|
207
236
|
principal.isClay = false;
|
|
237
|
+
principal.isProjectOwner = ownsProject(principal, status);
|
|
208
238
|
var envelope = { principal: principal, status: status, author: userAuthor(user) };
|
|
209
239
|
return createProjectBound(envelope, function () { return envelope; });
|
|
210
240
|
}
|
|
@@ -279,40 +309,85 @@ function attachProjectLogsService(ctx) {
|
|
|
279
309
|
return revalidate().author;
|
|
280
310
|
}
|
|
281
311
|
|
|
312
|
+
function currentStatus() {
|
|
313
|
+
var current = revalidate();
|
|
314
|
+
var project = projectFor(current.principal.projectSlug);
|
|
315
|
+
if (!project) throw new Error("Project not found.");
|
|
316
|
+
return project.getStatus();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function mutationContext() {
|
|
320
|
+
return recordContext(currentStatus());
|
|
321
|
+
}
|
|
322
|
+
|
|
282
323
|
// A session may write canonically; a user may only comment. The capability
|
|
283
324
|
// is decided by the binder that produced this envelope, never by a flag on
|
|
284
325
|
// the request.
|
|
285
326
|
var canonical = initial.principal.kind === "session";
|
|
327
|
+
var canDelete = canonical || initial.principal.isProjectOwner === true;
|
|
286
328
|
return {
|
|
287
329
|
isClay: false,
|
|
288
330
|
canWrite: canonical,
|
|
289
331
|
canComment: true,
|
|
332
|
+
canDelete: canDelete,
|
|
290
333
|
projectSlug: initial.principal.projectSlug,
|
|
291
334
|
scopeId: store.scopeId,
|
|
292
335
|
root: store.root,
|
|
293
336
|
author: initial.author,
|
|
294
|
-
listLogs: function (args) {
|
|
295
|
-
|
|
337
|
+
listLogs: function (args) {
|
|
338
|
+
var status = currentStatus();
|
|
339
|
+
var options = scopedArgs(args, status);
|
|
340
|
+
var result = live().list(options);
|
|
341
|
+
result.contextMode = options.contextMode;
|
|
342
|
+
result.currentChangeSetId = status.changeSetId || null;
|
|
343
|
+
result.isWorktree = status.isWorktree === true;
|
|
344
|
+
return result;
|
|
345
|
+
},
|
|
346
|
+
searchLogs: function (args) {
|
|
347
|
+
var status = currentStatus();
|
|
348
|
+
var options = scopedArgs(args, status);
|
|
349
|
+
var result = live().search(options);
|
|
350
|
+
result.contextMode = options.contextMode;
|
|
351
|
+
result.currentChangeSetId = status.changeSetId || null;
|
|
352
|
+
result.isWorktree = status.isWorktree === true;
|
|
353
|
+
return result;
|
|
354
|
+
},
|
|
296
355
|
readLog: function (args) {
|
|
297
356
|
var entry = live().read(args && args.ref, false);
|
|
298
357
|
if (!entry) throw new Error("Log entry not found.");
|
|
299
358
|
return entry;
|
|
300
359
|
},
|
|
301
360
|
logHistory: function (args) { return live().history(args && args.ref, args || {}); },
|
|
302
|
-
commentLog: function (args) { return live().comment(args && args.ref, args || {}, author()); },
|
|
361
|
+
commentLog: function (args) { return live().comment(args && args.ref, args || {}, author(), mutationContext()); },
|
|
303
362
|
readLogRevision: function (args) { return live().readRevision(args && args.ref, args && args.revision); },
|
|
304
363
|
listLogFeedback: canonical ? function (args) { return live().feedback(args || {}); } : driverOnly,
|
|
305
|
-
reviewLogComment: canonical ? function (args) { return live().review(args && args.ref, args || {}, author()); } : driverOnly,
|
|
364
|
+
reviewLogComment: canonical ? function (args) { return live().review(args && args.ref, args || {}, author(), mutationContext()); } : driverOnly,
|
|
306
365
|
revertLog: canonical
|
|
307
|
-
? function (args) { return live().revert(args && args.ref, args && args.revision, args && args.reason, author()); }
|
|
366
|
+
? function (args) { return live().revert(args && args.ref, args && args.revision, args && args.reason, author(), mutationContext()); }
|
|
308
367
|
: driverOnly,
|
|
309
|
-
createLog: canonical ? function (args) { return live().create(args || {}, author()); } : agentOnly,
|
|
310
|
-
updateLog: canonical ? function (args) { return live().update(args && args.ref, args || {}, author()); } : agentOnly,
|
|
311
|
-
linkLog: canonical ? function (args) { return live().link(args && args.ref, args && args.links, author()); } : agentOnly,
|
|
312
|
-
removeLog:
|
|
368
|
+
createLog: canonical ? function (args) { return live().create(args || {}, author(), mutationContext()); } : agentOnly,
|
|
369
|
+
updateLog: canonical ? function (args) { return live().update(args && args.ref, args || {}, author(), mutationContext()); } : agentOnly,
|
|
370
|
+
linkLog: canonical ? function (args) { return live().link(args && args.ref, args && args.links, author(), mutationContext()); } : agentOnly,
|
|
371
|
+
removeLog: canDelete ? function (args) {
|
|
372
|
+
var current = revalidate();
|
|
373
|
+
var project = projectFor(current.principal.projectSlug);
|
|
374
|
+
var currentStatus = project && project.getStatus();
|
|
375
|
+
if (!project || !authorizedProject(current.principal, currentStatus)) {
|
|
376
|
+
throw new Error("Project Logs are not available for this project.");
|
|
377
|
+
}
|
|
378
|
+
if (current.principal.kind !== "session" && !ownsProject(current.principal, currentStatus)) ownerOnly();
|
|
379
|
+
return storeForStatus(currentStatus).remove(args && args.ref, current.author, recordContext(currentStatus));
|
|
380
|
+
} : ownerOnly,
|
|
313
381
|
};
|
|
314
382
|
}
|
|
315
383
|
|
|
384
|
+
function setContextState(projectSlug, status) {
|
|
385
|
+
var project = projectFor(projectSlug);
|
|
386
|
+
var projectStatus = project && project.getStatus();
|
|
387
|
+
if (!projectStatus || !projectStatus.isWorktree || !projectStatus.changeSetId) return false;
|
|
388
|
+
return storeForStatus(projectStatus).setContextState(recordContext(projectStatus), status);
|
|
389
|
+
}
|
|
390
|
+
|
|
316
391
|
function createClayBound(revalidate) {
|
|
317
392
|
function target(args) {
|
|
318
393
|
var principal = revalidate();
|
|
@@ -325,6 +400,7 @@ function attachProjectLogsService(ctx) {
|
|
|
325
400
|
isClay: true,
|
|
326
401
|
canWrite: false,
|
|
327
402
|
canComment: false,
|
|
403
|
+
canDelete: false,
|
|
328
404
|
projectSlug: null,
|
|
329
405
|
author: null,
|
|
330
406
|
listLogs: function (args) { return target(args).list(args || {}); },
|
|
@@ -351,6 +427,7 @@ function attachProjectLogsService(ctx) {
|
|
|
351
427
|
bindProjectSession: bindProjectSession,
|
|
352
428
|
bindUser: bindUser,
|
|
353
429
|
bindMate: bindMate,
|
|
430
|
+
setContextState: setContextState,
|
|
354
431
|
};
|
|
355
432
|
}
|
|
356
433
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// shape, so history is deterministic across the boundary.
|
|
16
16
|
|
|
17
17
|
var logsSchema = require("./project-logs-schema");
|
|
18
|
+
var logContext = require("./project-log-context");
|
|
18
19
|
|
|
19
20
|
// Ops that advance the canonical record. `comment` and a non-incorporating
|
|
20
21
|
// `review` are participation, not revisions, and never appear here.
|
|
@@ -146,6 +147,7 @@ function revisions(chain, maxLinks) {
|
|
|
146
147
|
op: record.op === "review" ? "incorporate" : record.op,
|
|
147
148
|
at: record.at || 0,
|
|
148
149
|
author: record.author || null,
|
|
150
|
+
context: record.context || null,
|
|
149
151
|
snapshot: state,
|
|
150
152
|
changed: record.op === "create" ? [] : changedFields(previous, state),
|
|
151
153
|
// Present only on a revert, so a reader can see where it came from.
|
|
@@ -176,6 +178,7 @@ function describeRevision(entry) {
|
|
|
176
178
|
op: entry.op,
|
|
177
179
|
at: entry.at,
|
|
178
180
|
author: entry.author,
|
|
181
|
+
context: logContext.normalizeRecordContext(entry.context),
|
|
179
182
|
changed: entry.changed,
|
|
180
183
|
revertedFrom: entry.revertedFrom,
|
|
181
184
|
reason: entry.reason,
|
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
// Project Logs projection over the Clay-wide knowledge record backend.
|
|
2
|
-
//
|
|
3
|
-
// Project Logs are dry, factual project records: decisions, investigations,
|
|
4
|
-
// session notes, runbooks, references, incidents, and progress. They carry no
|
|
5
|
-
// persona and no autobiographical memory; that separation is a product rule,
|
|
6
|
-
// not a storage detail, so nothing here writes identity-shaped content.
|
|
7
|
-
//
|
|
8
|
-
// A worktree shares its parent project's Logs. The store root is resolved
|
|
9
|
-
// through Git's common directory, so a worktree session and a parent session
|
|
10
|
-
// read and write the same record file instead of forking project knowledge.
|
|
11
2
|
|
|
12
3
|
var fs = require("fs");
|
|
13
4
|
var path = require("path");
|
|
@@ -19,6 +10,8 @@ var logsSnapshot = require("./project-logs-snapshot");
|
|
|
19
10
|
var logsComments = require("./project-logs-comments");
|
|
20
11
|
var logsVersioning = require("./project-logs-versioning");
|
|
21
12
|
var logsRoot = require("./project-logs-root");
|
|
13
|
+
var logContext = require("./project-log-context");
|
|
14
|
+
var contextState = require("./project-logs-context-state");
|
|
22
15
|
|
|
23
16
|
var MAX_SUMMARY_CHARS = logsSchema.MAX_SUMMARY_CHARS;
|
|
24
17
|
var MAX_COMMENT_CHARS = 4000;
|
|
@@ -56,18 +49,17 @@ function validateKind(kind, fallback) {
|
|
|
56
49
|
function createProjectLogsStore(opts) {
|
|
57
50
|
var options = opts || {};
|
|
58
51
|
var root = options.root ? path.resolve(options.root) : logsRoot.resolveProjectRoot(options.cwd, options.runGit);
|
|
59
|
-
var scopeId =
|
|
52
|
+
var scopeId = options.projectKnowledgeId
|
|
53
|
+
? logsRoot.scopeIdForKnowledgeId(options.projectKnowledgeId)
|
|
54
|
+
: logsRoot.scopeIdForRoot(root);
|
|
60
55
|
var store = recordStore.createRecordStore({ scopeId: scopeId, baseDir: options.baseDir });
|
|
56
|
+
var contexts = contextState.attachContextState(store, scopeId);
|
|
61
57
|
|
|
62
|
-
// Stable across every revision of an entry: derived from the scope and the
|
|
63
|
-
// chain root, never from a path or an internal record id.
|
|
64
58
|
function refFor(rootId) {
|
|
65
59
|
var digest = crypto.createHash("sha256").update(scopeId + "\u0000" + String(rootId)).digest("base64url");
|
|
66
60
|
return "log:" + digest.substring(0, 24);
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
// File order is causal order. Wall-clock timestamps are recorded but never
|
|
70
|
-
// used to order a chain, so a skewed clock cannot reorder a revision.
|
|
71
63
|
function chains() {
|
|
72
64
|
var records = store.all();
|
|
73
65
|
var byRoot = new Map();
|
|
@@ -80,24 +72,19 @@ function createProjectLogsStore(opts) {
|
|
|
80
72
|
return byRoot;
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
|
|
84
|
-
// discussion is folded separately. Neither comments nor a clarify/decline
|
|
85
|
-
// review touch the canonical state or the revision count.
|
|
86
|
-
function fold(rootId, chain) {
|
|
75
|
+
function fold(rootId, chain, knownContextStates) {
|
|
87
76
|
var history = logsSnapshot.revisions(chain, MAX_LINKS);
|
|
88
77
|
if (!history.started || history.revisions.length === 0) return null;
|
|
89
78
|
var last = history.revisions[history.revisions.length - 1];
|
|
90
79
|
var first = history.revisions[0];
|
|
80
|
+
var originRecord = null;
|
|
81
|
+
for (var ci = 0; ci < chain.length; ci++) {
|
|
82
|
+
if (chain[ci].id === first.sourceRecordId) { originRecord = chain[ci]; break; }
|
|
83
|
+
}
|
|
91
84
|
|
|
92
|
-
// A create record that does not carry the minimum a log entry needs is not
|
|
93
|
-
// a log entry. Hand-edited files and torn writes that were later completed
|
|
94
|
-
// can look structurally valid without being usable.
|
|
95
85
|
if (!logsSchema.safeCategory(first.snapshot.category)) return null;
|
|
96
86
|
if (!first.snapshot.title || !first.snapshot.title.trim()) return null;
|
|
97
87
|
|
|
98
|
-
// Each reconstructed revision knows exactly which record produced it, so
|
|
99
|
-
// an incorporating review maps to its revision by identity. Two reviews
|
|
100
|
-
// sharing a millisecond cannot be confused for one another.
|
|
101
88
|
var revisionForRecordId = {};
|
|
102
89
|
for (var v = 0; v < history.revisions.length; v++) {
|
|
103
90
|
var source = history.revisions[v].sourceRecordId;
|
|
@@ -128,7 +115,12 @@ function createProjectLogsStore(opts) {
|
|
|
128
115
|
deleted: !!history.deleted,
|
|
129
116
|
deletedAt: history.deleted ? history.deleted.at : null,
|
|
130
117
|
deletedBy: history.deleted ? history.deleted.author : null,
|
|
118
|
+
context: logContext.normalizeRecordContext(originRecord && originRecord.context),
|
|
131
119
|
};
|
|
120
|
+
if (entry.context.changeSetId) {
|
|
121
|
+
var states = knownContextStates || contexts.states();
|
|
122
|
+
entry.context.status = states[entry.context.changeSetId] || entry.context.status;
|
|
123
|
+
}
|
|
132
124
|
// Entries written before summaries existed get a deterministic one derived
|
|
133
125
|
// from their own body. Nothing on disk is rewritten.
|
|
134
126
|
entry.summary = logsSchema.fallbackSummary(entry);
|
|
@@ -142,8 +134,9 @@ function createProjectLogsStore(opts) {
|
|
|
142
134
|
|
|
143
135
|
function entries(includeDeleted) {
|
|
144
136
|
var out = [];
|
|
137
|
+
var states = contexts.states();
|
|
145
138
|
chains().forEach(function (chain, rootId) {
|
|
146
|
-
var entry = fold(rootId, chain);
|
|
139
|
+
var entry = fold(rootId, chain, states);
|
|
147
140
|
if (!entry) return;
|
|
148
141
|
if (entry.deleted && !includeDeleted) return;
|
|
149
142
|
out.push(entry);
|
|
@@ -181,7 +174,7 @@ function createProjectLogsStore(opts) {
|
|
|
181
174
|
return located;
|
|
182
175
|
}
|
|
183
176
|
|
|
184
|
-
function create(input, author) {
|
|
177
|
+
function create(input, author, context) {
|
|
185
178
|
var data = input || {};
|
|
186
179
|
var validated = logsSchema.validateNewRecord(data);
|
|
187
180
|
var title = cleanLine(data.title || "", MAX_TITLE_CHARS);
|
|
@@ -213,6 +206,7 @@ function createProjectLogsStore(opts) {
|
|
|
213
206
|
links: snapshot.links,
|
|
214
207
|
snapshot: snapshot,
|
|
215
208
|
author: normalizeAuthor(author),
|
|
209
|
+
context: logContext.normalizeRecordContext(context),
|
|
216
210
|
at: Date.now(),
|
|
217
211
|
});
|
|
218
212
|
return read(refFor(id), false);
|
|
@@ -243,7 +237,7 @@ function createProjectLogsStore(opts) {
|
|
|
243
237
|
return logsSnapshot.cloneSnapshot(entry);
|
|
244
238
|
}
|
|
245
239
|
|
|
246
|
-
function update(ref, changes, author) {
|
|
240
|
+
function update(ref, changes, author, context) {
|
|
247
241
|
var located = requireLive(ref);
|
|
248
242
|
var data = changes || {};
|
|
249
243
|
var validated = logsSchema.validateRevision(data);
|
|
@@ -252,7 +246,7 @@ function createProjectLogsStore(opts) {
|
|
|
252
246
|
if (logsSnapshot.sameSnapshot(current, snapshot)) {
|
|
253
247
|
throw new Error("An update requires at least one changed field.");
|
|
254
248
|
}
|
|
255
|
-
appendRevision(located.rootId, "update", snapshot, author, {});
|
|
249
|
+
appendRevision(located.rootId, "update", snapshot, author, { context: logContext.normalizeRecordContext(context) });
|
|
256
250
|
return read(ref, false);
|
|
257
251
|
}
|
|
258
252
|
|
|
@@ -284,7 +278,7 @@ function createProjectLogsStore(opts) {
|
|
|
284
278
|
// Human participation. Append-only for this slice: no edit, no delete, and
|
|
285
279
|
// never a canonical revision. The author is supplied by the authorization
|
|
286
280
|
// layer, never by the caller's payload.
|
|
287
|
-
function comment(ref, input, author) {
|
|
281
|
+
function comment(ref, input, author, context) {
|
|
288
282
|
var located = requireLive(ref);
|
|
289
283
|
var data = input || {};
|
|
290
284
|
var body = cleanText(data.body || "", MAX_COMMENT_CHARS);
|
|
@@ -300,12 +294,13 @@ function createProjectLogsStore(opts) {
|
|
|
300
294
|
scope: scopeId,
|
|
301
295
|
body: body,
|
|
302
296
|
author: normalizeAuthor(author),
|
|
297
|
+
context: logContext.normalizeRecordContext(context),
|
|
303
298
|
at: Date.now(),
|
|
304
299
|
});
|
|
305
300
|
return read(ref, false);
|
|
306
301
|
}
|
|
307
302
|
|
|
308
|
-
function link(ref, links, author) {
|
|
303
|
+
function link(ref, links, author, context) {
|
|
309
304
|
var located = requireLive(ref);
|
|
310
305
|
var normalized = normalizeLinks(links);
|
|
311
306
|
if (normalized.length === 0) throw new Error("At least one link reference is required.");
|
|
@@ -321,11 +316,11 @@ function createProjectLogsStore(opts) {
|
|
|
321
316
|
if (logsSnapshot.sameSnapshot(current, snapshot)) {
|
|
322
317
|
throw new Error("Those links are already attached to this entry.");
|
|
323
318
|
}
|
|
324
|
-
appendRevision(located.rootId, "link", snapshot, author, {});
|
|
319
|
+
appendRevision(located.rootId, "link", snapshot, author, { context: logContext.normalizeRecordContext(context) });
|
|
325
320
|
return read(ref, false);
|
|
326
321
|
}
|
|
327
322
|
|
|
328
|
-
function remove(ref, author) {
|
|
323
|
+
function remove(ref, author, context) {
|
|
329
324
|
var located = requireLive(ref);
|
|
330
325
|
store.append({
|
|
331
326
|
id: recordStore.newRecordId(),
|
|
@@ -333,6 +328,7 @@ function createProjectLogsStore(opts) {
|
|
|
333
328
|
op: "delete",
|
|
334
329
|
scope: scopeId,
|
|
335
330
|
author: normalizeAuthor(author),
|
|
331
|
+
context: logContext.normalizeRecordContext(context),
|
|
336
332
|
at: Date.now(),
|
|
337
333
|
});
|
|
338
334
|
return read(ref, true);
|
|
@@ -473,6 +469,7 @@ function createProjectLogsStore(opts) {
|
|
|
473
469
|
readRevision: versioning.readRevision,
|
|
474
470
|
link: link,
|
|
475
471
|
remove: remove,
|
|
472
|
+
setContextState: contexts.set,
|
|
476
473
|
read: read,
|
|
477
474
|
list: list,
|
|
478
475
|
search: search,
|
|
@@ -9,6 +9,7 @@ var logsSchema = require("./project-logs-schema");
|
|
|
9
9
|
var logsSnapshot = require("./project-logs-snapshot");
|
|
10
10
|
var logsComments = require("./project-logs-comments");
|
|
11
11
|
var recordStore = require("./knowledge-record-store");
|
|
12
|
+
var logContext = require("./project-log-context");
|
|
12
13
|
|
|
13
14
|
function attachVersioning(ctx) {
|
|
14
15
|
var read = ctx.read;
|
|
@@ -27,7 +28,7 @@ function attachVersioning(ctx) {
|
|
|
27
28
|
// create no canonical revision. `incorporate` appends exactly one record that
|
|
28
29
|
// both resolves the comment and carries the new canonical snapshot, so the
|
|
29
30
|
// two can never drift apart.
|
|
30
|
-
function review(ref, input, author) {
|
|
31
|
+
function review(ref, input, author, context) {
|
|
31
32
|
var located = requireLive(ref);
|
|
32
33
|
var data = input || {};
|
|
33
34
|
if (!logsComments.isAction(data.action)) {
|
|
@@ -56,6 +57,7 @@ function attachVersioning(ctx) {
|
|
|
56
57
|
commentId: data.commentId,
|
|
57
58
|
response: response,
|
|
58
59
|
author: normalizeAuthor(author),
|
|
60
|
+
context: logContext.normalizeRecordContext(context),
|
|
59
61
|
at: Date.now(),
|
|
60
62
|
});
|
|
61
63
|
return read(ref, false);
|
|
@@ -71,13 +73,14 @@ function attachVersioning(ctx) {
|
|
|
71
73
|
action: "incorporate",
|
|
72
74
|
commentId: data.commentId,
|
|
73
75
|
response: response,
|
|
76
|
+
context: logContext.normalizeRecordContext(context),
|
|
74
77
|
});
|
|
75
78
|
return read(ref, false);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
// Restore an earlier revision by appending a new one. Later history is never
|
|
79
82
|
// erased: a revert is itself a revision, with the source recorded.
|
|
80
|
-
function revert(ref, revision, reason, author) {
|
|
83
|
+
function revert(ref, revision, reason, author, context) {
|
|
81
84
|
var located = requireLive(ref);
|
|
82
85
|
var target = Number(revision);
|
|
83
86
|
if (!Number.isInteger(target) || target < 1) throw new Error("A revision number is required.");
|
|
@@ -96,6 +99,7 @@ function attachVersioning(ctx) {
|
|
|
96
99
|
appendRevision(located.rootId, "revert", logsSnapshot.cloneSnapshot(source.snapshot), author, {
|
|
97
100
|
revertedFrom: target,
|
|
98
101
|
reason: trimmedReason,
|
|
102
|
+
context: logContext.normalizeRecordContext(context),
|
|
99
103
|
});
|
|
100
104
|
return read(ref, false);
|
|
101
105
|
}
|
package/lib/project-logs.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Authorship is split deliberately. Canonical entries are created and revised
|
|
9
9
|
// only by this project's agent sessions through `clay-logs`; a connected human
|
|
10
|
-
// reads the ledger
|
|
11
|
-
//
|
|
10
|
+
// reads the ledger, appends attributed comments, and lets the project owner
|
|
11
|
+
// delete an entry. That keeps the log an honest record rather than a wiki.
|
|
12
12
|
|
|
13
13
|
var logsMcp = require("./project-logs-mcp-server");
|
|
14
14
|
|
|
@@ -19,7 +19,7 @@ var MAX_NOTICE_SUMMARY_CHARS = 240;
|
|
|
19
19
|
// Canonical writes that move the ledger forward. A plain comment, a
|
|
20
20
|
// clarification, and a decline are participation, not revisions, so they never
|
|
21
21
|
// notify.
|
|
22
|
-
var NOTIFYING_OPS = ["create", "update", "link", "incorporate", "revert"];
|
|
22
|
+
var NOTIFYING_OPS = ["create", "update", "link", "incorporate", "revert", "delete"];
|
|
23
23
|
|
|
24
24
|
function clip(value, max) {
|
|
25
25
|
if (typeof value !== "string") return "";
|
|
@@ -71,6 +71,7 @@ var HANDLED_TYPES = [
|
|
|
71
71
|
"project_logs_list",
|
|
72
72
|
"project_log_read",
|
|
73
73
|
"project_log_comment",
|
|
74
|
+
"project_log_delete",
|
|
74
75
|
// Retired human mutations. Still claimed so an older client gets an
|
|
75
76
|
// explicit refusal rather than silence.
|
|
76
77
|
"project_log_create",
|
|
@@ -105,6 +106,21 @@ function attachProjectLogs(ctx) {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
function broadcastCommentReview(entry, request) {
|
|
110
|
+
if (isMate || !entry || !request || !request.commentId) return;
|
|
111
|
+
var notice = {
|
|
112
|
+
type: "project_log_comment_reviewed",
|
|
113
|
+
ref: entry.ref,
|
|
114
|
+
commentId: request.commentId,
|
|
115
|
+
action: request.action,
|
|
116
|
+
};
|
|
117
|
+
var clients = getClients();
|
|
118
|
+
for (var client of clients) {
|
|
119
|
+
if (!client || client.readyState !== 1 || client._clayPane) continue;
|
|
120
|
+
sendTo(client, notice);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
// Wrap a canonical write so a successful one notifies. A throw never reaches
|
|
109
125
|
// here, so a failed or no-op write is silent.
|
|
110
126
|
function notifying(op, run) {
|
|
@@ -149,13 +165,27 @@ function attachProjectLogs(ctx) {
|
|
|
149
165
|
// read and neither path dumps a record body into the list.
|
|
150
166
|
// Returns both the rows and this project's live category vocabulary, so the
|
|
151
167
|
// filter is populated solely from what this project actually uses.
|
|
152
|
-
function listEntries(bound, query, category) {
|
|
168
|
+
function listEntries(bound, query, category, contextMode) {
|
|
169
|
+
var options = { kind: category, limit: LIST_LIMIT, contextMode: contextMode };
|
|
153
170
|
if (query) {
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
options.query = query;
|
|
172
|
+
var hits = bound.searchLogs(options);
|
|
173
|
+
return {
|
|
174
|
+
entries: hits.results,
|
|
175
|
+
categories: hits.categories || [],
|
|
176
|
+
contextMode: hits.contextMode,
|
|
177
|
+
currentChangeSetId: hits.currentChangeSetId,
|
|
178
|
+
isWorktree: hits.isWorktree,
|
|
179
|
+
};
|
|
156
180
|
}
|
|
157
|
-
var listed = bound.listLogs(
|
|
158
|
-
return {
|
|
181
|
+
var listed = bound.listLogs(options);
|
|
182
|
+
return {
|
|
183
|
+
entries: listed.entries,
|
|
184
|
+
categories: listed.categories || [],
|
|
185
|
+
contextMode: listed.contextMode,
|
|
186
|
+
currentChangeSetId: listed.currentChangeSetId,
|
|
187
|
+
isWorktree: listed.isWorktree,
|
|
188
|
+
};
|
|
159
189
|
}
|
|
160
190
|
|
|
161
191
|
function handleLogsMessage(ws, msg) {
|
|
@@ -187,27 +217,40 @@ function attachProjectLogs(ctx) {
|
|
|
187
217
|
var query = typeof msg.query === "string" ? msg.query.trim() : "";
|
|
188
218
|
// A view preference, not an authorization input: the store validates it.
|
|
189
219
|
var category = typeof msg.category === "string" && msg.category ? msg.category : null;
|
|
190
|
-
var
|
|
220
|
+
var contextMode = typeof msg.contextMode === "string" ? msg.contextMode : null;
|
|
221
|
+
var listing = listEntries(bound, query, category, contextMode);
|
|
191
222
|
sendTo(ws, {
|
|
192
223
|
type: "project_logs_state",
|
|
193
224
|
requestId: requestId,
|
|
194
225
|
entries: listing.entries,
|
|
195
226
|
categories: listing.categories,
|
|
227
|
+
contextMode: listing.contextMode,
|
|
228
|
+
currentChangeSetId: listing.currentChangeSetId,
|
|
229
|
+
isWorktree: listing.isWorktree === true,
|
|
230
|
+
canDelete: bound.canDelete === true,
|
|
196
231
|
});
|
|
197
232
|
return true;
|
|
198
233
|
}
|
|
199
234
|
|
|
200
235
|
if (msg.type === "project_log_read") {
|
|
201
|
-
sendTo(ws, { type: "project_log_entry", requestId: requestId, entry: bound.readLog({ ref: msg.ref }) });
|
|
236
|
+
sendTo(ws, { type: "project_log_entry", requestId: requestId, entry: bound.readLog({ ref: msg.ref }), canDelete: bound.canDelete === true });
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (msg.type === "project_log_delete") {
|
|
241
|
+
var removed = bound.removeLog({ ref: msg.ref });
|
|
242
|
+
sendTo(ws, { type: "project_log_deleted", requestId: requestId, ref: removed.ref });
|
|
243
|
+
broadcastUpdate(removed, "delete");
|
|
202
244
|
return true;
|
|
203
245
|
}
|
|
204
246
|
|
|
205
247
|
if (msg.type === "project_log_comment") {
|
|
206
248
|
var commented = bound.commentLog({ ref: msg.ref, body: msg.body });
|
|
207
|
-
|
|
208
|
-
try { onFeedback(commented); } catch (deliveryError) {
|
|
249
|
+
var reviewQueued = false;
|
|
250
|
+
try { reviewQueued = onFeedback(commented) === true; } catch (deliveryError) {
|
|
209
251
|
console.error("[project-logs] Failed to deliver comment:", deliveryError.message || deliveryError);
|
|
210
252
|
}
|
|
253
|
+
sendTo(ws, { type: "project_log_commented", requestId: requestId, entry: commented, reviewQueued: reviewQueued });
|
|
211
254
|
return true;
|
|
212
255
|
}
|
|
213
256
|
} catch (e) {
|
|
@@ -232,6 +275,7 @@ function attachProjectLogs(ctx) {
|
|
|
232
275
|
reviewLogComment: function (args) {
|
|
233
276
|
var request = coerceToolArgs(args);
|
|
234
277
|
var result = bound.reviewLogComment(request);
|
|
278
|
+
broadcastCommentReview(result, request);
|
|
235
279
|
// Only an incorporation is a canonical revision; clarify and decline
|
|
236
280
|
// resolve a comment and change nothing.
|
|
237
281
|
if (request && request.action === "incorporate") broadcastUpdate(result, "incorporate");
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Idempotent acknowledgement support for ordinary project chat messages.
|
|
2
|
+
|
|
3
|
+
var CLIENT_MESSAGE_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
4
|
+
|
|
5
|
+
function normalizeClientMessageId(value) {
|
|
6
|
+
if (typeof value !== "string" || !CLIENT_MESSAGE_ID_RE.test(value)) return null;
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ownerKey(ws) {
|
|
11
|
+
return ws && ws._clayUser && ws._clayUser.id ? String(ws._clayUser.id) : "_default";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function deliveryKey(ownerId, clientMessageId) {
|
|
15
|
+
return ownerId + ":" + clientMessageId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureRecordedMessages(session) {
|
|
19
|
+
if (session._recordedClientMessages) return session._recordedClientMessages;
|
|
20
|
+
var recorded = new Map();
|
|
21
|
+
var history = Array.isArray(session.history) ? session.history : [];
|
|
22
|
+
for (var i = 0; i < history.length; i++) {
|
|
23
|
+
var item = history[i];
|
|
24
|
+
var clientMessageId = normalizeClientMessageId(item && item.clientMessageId);
|
|
25
|
+
if (!clientMessageId) continue;
|
|
26
|
+
recorded.set(deliveryKey(item.from ? String(item.from) : "_default", clientMessageId), item);
|
|
27
|
+
}
|
|
28
|
+
session._recordedClientMessages = recorded;
|
|
29
|
+
return recorded;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createProjectMessageDelivery(sendTo) {
|
|
33
|
+
function inspect(ws, session, msg) {
|
|
34
|
+
var clientMessageId = normalizeClientMessageId(msg && msg.clientMessageId);
|
|
35
|
+
if (!clientMessageId) return { clientMessageId: null, ownerId: ownerKey(ws), duplicate: false };
|
|
36
|
+
var ownerId = ownerKey(ws);
|
|
37
|
+
var recordedMessage = ensureRecordedMessages(session).get(deliveryKey(ownerId, clientMessageId)) || null;
|
|
38
|
+
return {
|
|
39
|
+
clientMessageId: clientMessageId,
|
|
40
|
+
ownerId: ownerId,
|
|
41
|
+
duplicate: !!recordedMessage,
|
|
42
|
+
recordedMessage: recordedMessage,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function markRecorded(session, receipt, message) {
|
|
47
|
+
if (!receipt || !receipt.clientMessageId) return;
|
|
48
|
+
ensureRecordedMessages(session).set(deliveryKey(receipt.ownerId, receipt.clientMessageId), message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function acknowledge(ws, receipt, message) {
|
|
52
|
+
if (!receipt || !receipt.clientMessageId) return;
|
|
53
|
+
sendTo(ws, {
|
|
54
|
+
type: "message_ack",
|
|
55
|
+
clientMessageId: receipt.clientMessageId,
|
|
56
|
+
message: message || receipt.recordedMessage || null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
inspect: inspect,
|
|
62
|
+
markRecorded: markRecorded,
|
|
63
|
+
acknowledge: acknowledge,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
createProjectMessageDelivery: createProjectMessageDelivery,
|
|
69
|
+
normalizeClientMessageId: normalizeClientMessageId,
|
|
70
|
+
};
|