clay-server 4.1.0-beta.10 → 4.1.0-beta.11
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/loop-registry-occurrence.js +1 -0
- package/lib/project-connection.js +2 -0
- package/lib/project-logs-schema.js +1 -1
- package/lib/project-logs-service.js +46 -13
- package/lib/project-logs-store.js +17 -15
- package/lib/project-logs.js +70 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-scheduled-result-store.js +131 -0
- package/lib/project-scheduled-task-execution.js +35 -2
- package/lib/project.js +6 -0
- package/lib/public/app.js +6 -0
- package/lib/public/css/notifications-center.css +3 -3
- package/lib/public/css/project-logs.css +15 -0
- package/lib/public/modules/app-messages.js +13 -1
- package/lib/public/modules/project-logs-render.js +11 -1
- package/lib/public/modules/project-logs.js +41 -6
- package/lib/public/modules/scheduled-result-client.js +33 -0
- package/lib/public/modules/scheduled-result-display.js +50 -0
- package/lib/public/modules/scheduled-result-notification.js +31 -0
- package/lib/public/modules/scheduled-task-presentation.js +15 -0
- package/lib/public/modules/scheduled-tasks.js +4 -15
- package/lib/scheduled-run-state.js +10 -2
- package/lib/scheduler.js +1 -0
- package/lib/session-pair-factory.js +2 -2
- package/lib/sessions.js +4 -0
- package/lib/ws-schema.js +5 -0
- package/package.json +1 -1
|
@@ -38,6 +38,7 @@ function attachLoopRegistryOccurrence(ctx) {
|
|
|
38
38
|
ctx.scheduledRuns.prepareRecord(rec, prepared);
|
|
39
39
|
if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return { ok: false, error: ctx.lastSaveError() || "The occurrence reservation could not be stored." }; }
|
|
40
40
|
ctx.changed();
|
|
41
|
+
if (prepared && prepared.skippedRun) ctx.scheduledRuns.notifyTerminal(rec, prepared.skippedRun);
|
|
41
42
|
return { ok: true, value: prepared };
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -77,6 +77,7 @@ function attachConnection(ctx) {
|
|
|
77
77
|
var _loop = ctx._loop;
|
|
78
78
|
var _mcp = ctx._mcp;
|
|
79
79
|
var _notifications = ctx._notifications;
|
|
80
|
+
var _projectLogs = ctx._projectLogs;
|
|
80
81
|
var _splitGroups = ctx._splitGroups;
|
|
81
82
|
var _vendorLogin = ctx._vendorLogin;
|
|
82
83
|
var hydrateImageRefs = ctx.hydrateImageRefs;
|
|
@@ -229,6 +230,7 @@ function attachConnection(ctx) {
|
|
|
229
230
|
_loop.sendConnectionState(ws);
|
|
230
231
|
if (_mcp) _mcp.sendConnectionState(ws);
|
|
231
232
|
if (_notifications) _notifications.sendConnectionState(ws, sendTo);
|
|
233
|
+
if (_projectLogs) _projectLogs.sendScheduledResultState(ws);
|
|
232
234
|
|
|
233
235
|
// Session list (filtered for access control). Uses the same mapper as
|
|
234
236
|
// broadcastSessionList: a hand-rolled copy here silently drifts and
|
|
@@ -196,7 +196,7 @@ function cleanLine(value, max) {
|
|
|
196
196
|
|
|
197
197
|
function normalizeAuthor(author) {
|
|
198
198
|
var source = author || {};
|
|
199
|
-
var type = source.type === "user" ? "user" : "session";
|
|
199
|
+
var type = source.type === "user" ? "user" : (source.type === "system" ? "system" : "session");
|
|
200
200
|
return {
|
|
201
201
|
type: type,
|
|
202
202
|
userId: source.userId ? cleanLine(String(source.userId), 120) : null,
|
|
@@ -253,6 +253,11 @@ function attachProjectLogsService(ctx) {
|
|
|
253
253
|
|
|
254
254
|
function createProjectBound(initial, revalidate) {
|
|
255
255
|
var store = storeForStatus(initial.status);
|
|
256
|
+
var scheduledResultViewerId = initial.principal.userId || null;
|
|
257
|
+
|
|
258
|
+
function withScheduledResultViewer(args) {
|
|
259
|
+
return Object.assign({}, args || {}, { scheduledResultOwnerId: scheduledResultViewerId });
|
|
260
|
+
}
|
|
256
261
|
|
|
257
262
|
// Identity, authorization, and the target store are all re-derived per
|
|
258
263
|
// call. revalidate() re-checks exact session identity where there is one;
|
|
@@ -288,7 +293,15 @@ function attachProjectLogsService(ctx) {
|
|
|
288
293
|
|
|
289
294
|
function scopedEntry(args) {
|
|
290
295
|
var scope = currentScope();
|
|
291
|
-
|
|
296
|
+
var entry = visibleEntry(live(), args && args.ref, scope.status, scope.worktreeOnly);
|
|
297
|
+
if (entry.scheduledResult && (entry.scheduledResult.ownerId || null) !== scheduledResultViewerId) throw new Error("Log entry not found.");
|
|
298
|
+
return entry;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function mutableEntry(args) {
|
|
302
|
+
var entry = scopedEntry(args);
|
|
303
|
+
if (entry.scheduledResult) throw new Error("Scheduled results are server-owned records.");
|
|
304
|
+
return entry;
|
|
292
305
|
}
|
|
293
306
|
|
|
294
307
|
function mutationContext() {
|
|
@@ -312,7 +325,7 @@ function attachProjectLogsService(ctx) {
|
|
|
312
325
|
listLogs: function (args) {
|
|
313
326
|
var scope = currentScope();
|
|
314
327
|
var status = scope.status;
|
|
315
|
-
var options = scopedArgs(args, status, scope.worktreeOnly);
|
|
328
|
+
var options = withScheduledResultViewer(scopedArgs(args, status, scope.worktreeOnly));
|
|
316
329
|
var result = live().list(options);
|
|
317
330
|
result.contextMode = options.contextMode;
|
|
318
331
|
result.currentChangeSetId = status.changeSetId || null;
|
|
@@ -322,7 +335,7 @@ function attachProjectLogsService(ctx) {
|
|
|
322
335
|
searchLogs: function (args) {
|
|
323
336
|
var scope = currentScope();
|
|
324
337
|
var status = scope.status;
|
|
325
|
-
var options = scopedArgs(args, status, scope.worktreeOnly);
|
|
338
|
+
var options = withScheduledResultViewer(scopedArgs(args, status, scope.worktreeOnly));
|
|
326
339
|
var result = live().search(options);
|
|
327
340
|
result.contextMode = options.contextMode;
|
|
328
341
|
result.currentChangeSetId = status.changeSetId || null;
|
|
@@ -334,18 +347,20 @@ function attachProjectLogsService(ctx) {
|
|
|
334
347
|
},
|
|
335
348
|
logHistory: function (args) { scopedEntry(args); return live().history(args && args.ref, args || {}); },
|
|
336
349
|
commentLog: function (args) { scopedEntry(args); return live().comment(args && args.ref, args || {}, author(), mutationContext()); },
|
|
350
|
+
scheduledResultsState: function () { var scope = currentScope(); return live().unreadScheduledResults(scheduledResultViewerId, scopedArgs({}, scope.status, scope.worktreeOnly)); },
|
|
351
|
+
markScheduledResultRead: function (args) { var scope = currentScope(); scopedEntry(args); return live().markScheduledResultRead(args && args.ref, scheduledResultViewerId, scopedArgs({}, scope.status, scope.worktreeOnly)); },
|
|
337
352
|
readLogRevision: function (args) { scopedEntry(args); return live().readRevision(args && args.ref, args && args.revision); },
|
|
338
353
|
listLogFeedback: canonical ? function (args) {
|
|
339
354
|
var scope = currentScope();
|
|
340
|
-
return live().feedback(scopedArgs(args, scope.status, scope.worktreeOnly));
|
|
355
|
+
return live().feedback(withScheduledResultViewer(scopedArgs(args, scope.status, scope.worktreeOnly)));
|
|
341
356
|
} : driverOnly,
|
|
342
|
-
reviewLogComment: canonical ? function (args) { scopedEntry(args); return live().review(args && args.ref, args || {}, author(), mutationContext()); } : driverOnly,
|
|
357
|
+
reviewLogComment: canonical ? function (args) { if (args && args.action === "incorporate") mutableEntry(args); else scopedEntry(args); return live().review(args && args.ref, args || {}, author(), mutationContext()); } : driverOnly,
|
|
343
358
|
revertLog: canonical
|
|
344
|
-
? function (args) {
|
|
359
|
+
? function (args) { mutableEntry(args); return live().revert(args && args.ref, args && args.revision, args && args.reason, author(), mutationContext()); }
|
|
345
360
|
: driverOnly,
|
|
346
361
|
createLog: canonical ? function (args) { return live().create(args || {}, author(), mutationContext()); } : agentOnly,
|
|
347
|
-
updateLog: canonical ? function (args) {
|
|
348
|
-
linkLog: canonical ? function (args) {
|
|
362
|
+
updateLog: canonical ? function (args) { mutableEntry(args); return live().update(args && args.ref, args || {}, author(), mutationContext()); } : agentOnly,
|
|
363
|
+
linkLog: canonical ? function (args) { mutableEntry(args); return live().link(args && args.ref, args && args.links, author(), mutationContext()); } : agentOnly,
|
|
349
364
|
removeLog: canDelete ? function (args) {
|
|
350
365
|
var current = revalidate();
|
|
351
366
|
var project = projectFor(current.principal.projectSlug);
|
|
@@ -353,7 +368,7 @@ function attachProjectLogsService(ctx) {
|
|
|
353
368
|
if (!project || !authorizedProject(current.principal, currentStatus)) {
|
|
354
369
|
throw new Error("Project Logs are not available for this project.");
|
|
355
370
|
}
|
|
356
|
-
|
|
371
|
+
mutableEntry(args);
|
|
357
372
|
if (current.principal.kind !== "session" && !ownsProject(current.principal, currentStatus)) ownerOnly();
|
|
358
373
|
return storeForStatus(currentStatus).remove(args && args.ref, current.author, recordContext(currentStatus));
|
|
359
374
|
} : ownerOnly,
|
|
@@ -367,6 +382,20 @@ function attachProjectLogsService(ctx) {
|
|
|
367
382
|
return storeForStatus(projectStatus).setContextState(recordContext(projectStatus), status);
|
|
368
383
|
}
|
|
369
384
|
|
|
385
|
+
// Trusted server lifecycle path. It is deliberately not part of any bound
|
|
386
|
+
// human or MCP write capability, so system results cannot be impersonated.
|
|
387
|
+
function recordScheduledResult(source) {
|
|
388
|
+
var data = source || {};
|
|
389
|
+
var project = projectFor(data.projectSlug);
|
|
390
|
+
if (!project) throw new Error("Project not found.");
|
|
391
|
+
var status = project.getStatus();
|
|
392
|
+
if (!status || status.isMate === true) throw new Error("Scheduled results require a project.");
|
|
393
|
+
if (isMultiUser()) {
|
|
394
|
+
if (!data.ownerId || !findUserById(data.ownerId)) throw new Error("Scheduled result owner is unavailable.");
|
|
395
|
+
}
|
|
396
|
+
return storeForStatus(status).createScheduledResult(data, recordContext(status));
|
|
397
|
+
}
|
|
398
|
+
|
|
370
399
|
function createClayBound(revalidate) {
|
|
371
400
|
function target(args) {
|
|
372
401
|
var principal = revalidate();
|
|
@@ -374,14 +403,16 @@ function attachProjectLogsService(ctx) {
|
|
|
374
403
|
if (!slug) throw new Error("An exact project slug is required.");
|
|
375
404
|
var status = statusFor(principal, slug);
|
|
376
405
|
var decision = projectAuthorization(principal, status);
|
|
377
|
-
return { store: storeForStatus(status), status: status, worktreeOnly: decision.worktreeOnly };
|
|
406
|
+
return { store: storeForStatus(status), status: status, worktreeOnly: decision.worktreeOnly, viewerId: principal.userId || null };
|
|
378
407
|
}
|
|
379
408
|
|
|
380
409
|
function targetEntry(args) {
|
|
381
410
|
var selected = target(args);
|
|
411
|
+
var entry = visibleEntry(selected.store, args && args.ref, selected.status, selected.worktreeOnly);
|
|
412
|
+
if (entry.scheduledResult && (entry.scheduledResult.ownerId || null) !== selected.viewerId) throw new Error("Log entry not found.");
|
|
382
413
|
return {
|
|
383
414
|
selected: selected,
|
|
384
|
-
entry:
|
|
415
|
+
entry: entry,
|
|
385
416
|
};
|
|
386
417
|
}
|
|
387
418
|
|
|
@@ -405,6 +436,7 @@ function attachProjectLogsService(ctx) {
|
|
|
405
436
|
seenScopes.add(scopeKey);
|
|
406
437
|
var entry = null;
|
|
407
438
|
try { entry = visibleEntry(storeForStatus(status), ref, status, decision.worktreeOnly); } catch (e) { return; }
|
|
439
|
+
if (entry && entry.scheduledResult && (entry.scheduledResult.ownerId || null) !== (principal.userId || null)) return;
|
|
408
440
|
if (!entry) return;
|
|
409
441
|
var targetSlug = decision.worktreeOnly
|
|
410
442
|
? slug
|
|
@@ -425,11 +457,11 @@ function attachProjectLogsService(ctx) {
|
|
|
425
457
|
author: null,
|
|
426
458
|
listLogs: function (args) {
|
|
427
459
|
var selected = target(args);
|
|
428
|
-
return selected.store.list(scopedArgs(args, selected.status, selected.worktreeOnly));
|
|
460
|
+
return selected.store.list(Object.assign({}, scopedArgs(args, selected.status, selected.worktreeOnly), { scheduledResultOwnerId: selected.viewerId }));
|
|
429
461
|
},
|
|
430
462
|
searchLogs: function (args) {
|
|
431
463
|
var selected = target(args);
|
|
432
|
-
return selected.store.search(scopedArgs(args, selected.status, selected.worktreeOnly));
|
|
464
|
+
return selected.store.search(Object.assign({}, scopedArgs(args, selected.status, selected.worktreeOnly), { scheduledResultOwnerId: selected.viewerId }));
|
|
433
465
|
},
|
|
434
466
|
readLog: function (args) {
|
|
435
467
|
return targetEntry(args).entry;
|
|
@@ -458,6 +490,7 @@ function attachProjectLogsService(ctx) {
|
|
|
458
490
|
bindProjectSession: bindProjectSession,
|
|
459
491
|
bindUser: bindUser,
|
|
460
492
|
bindMate: bindMate,
|
|
493
|
+
recordScheduledResult: recordScheduledResult,
|
|
461
494
|
setContextState: setContextState,
|
|
462
495
|
};
|
|
463
496
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Project Logs projection over the Clay-wide knowledge record backend.
|
|
2
|
-
|
|
3
2
|
var fs = require("fs");
|
|
4
3
|
var path = require("path");
|
|
5
4
|
var crypto = require("crypto");
|
|
@@ -12,6 +11,7 @@ var logsVersioning = require("./project-logs-versioning");
|
|
|
12
11
|
var logsRoot = require("./project-logs-root");
|
|
13
12
|
var logContext = require("./project-log-context");
|
|
14
13
|
var contextState = require("./project-logs-context-state");
|
|
14
|
+
var scheduledResultStore = require("./project-scheduled-result-store");
|
|
15
15
|
|
|
16
16
|
var MAX_SUMMARY_CHARS = logsSchema.MAX_SUMMARY_CHARS;
|
|
17
17
|
var MAX_COMMENT_CHARS = 4000;
|
|
@@ -34,7 +34,6 @@ var normalizeTags = logsSchema.normalizeTags;
|
|
|
34
34
|
var normalizeLinks = logsSchema.normalizeLinks;
|
|
35
35
|
|
|
36
36
|
var page = logsQuery.page;
|
|
37
|
-
|
|
38
37
|
// Filters accept any well-formed category. An unknown but valid one matches
|
|
39
38
|
// nothing, which is a real answer. A supplied malformed one is an error:
|
|
40
39
|
// silently dropping it would widen the result set to everything the caller is
|
|
@@ -45,7 +44,6 @@ function validateKind(kind, fallback) {
|
|
|
45
44
|
return logsSchema.normalizeCategory(kind);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
|
|
49
47
|
function createProjectLogsStore(opts) {
|
|
50
48
|
var options = opts || {};
|
|
51
49
|
var root = options.root ? path.resolve(options.root) : logsRoot.resolveProjectRoot(options.cwd, options.runGit);
|
|
@@ -116,6 +114,7 @@ function createProjectLogsStore(opts) {
|
|
|
116
114
|
deletedAt: history.deleted ? history.deleted.at : null,
|
|
117
115
|
deletedBy: history.deleted ? history.deleted.author : null,
|
|
118
116
|
context: logContext.normalizeRecordContext(originRecord && originRecord.context),
|
|
117
|
+
scheduledResult: scheduledResultStore.normalizeMetadata(originRecord && originRecord.scheduledResult),
|
|
119
118
|
};
|
|
120
119
|
if (entry.context.changeSetId) {
|
|
121
120
|
var states = knownContextStates || contexts.states();
|
|
@@ -132,13 +131,14 @@ function createProjectLogsStore(opts) {
|
|
|
132
131
|
return entry;
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
function entries(includeDeleted) {
|
|
134
|
+
function entries(includeDeleted, scheduledResultOwnerId) {
|
|
136
135
|
var out = [];
|
|
137
136
|
var states = contexts.states();
|
|
138
137
|
chains().forEach(function (chain, rootId) {
|
|
139
138
|
var entry = fold(rootId, chain, states);
|
|
140
139
|
if (!entry) return;
|
|
141
140
|
if (entry.deleted && !includeDeleted) return;
|
|
141
|
+
if (!scheduledResultStore.visibleTo(entry.scheduledResult, scheduledResultOwnerId)) return;
|
|
142
142
|
out.push(entry);
|
|
143
143
|
});
|
|
144
144
|
// Latest canonical update first, ref as a deterministic tie-break. Only a
|
|
@@ -157,12 +157,13 @@ function createProjectLogsStore(opts) {
|
|
|
157
157
|
return found;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
function read(ref, includeDeleted) {
|
|
160
|
+
function read(ref, includeDeleted, scheduledResultOwnerId) {
|
|
161
161
|
var located = findChain(ref);
|
|
162
162
|
if (!located) return null;
|
|
163
163
|
var entry = fold(located.rootId, located.chain);
|
|
164
164
|
if (!entry) return null;
|
|
165
165
|
if (entry.deleted && !includeDeleted) return null;
|
|
166
|
+
if (!scheduledResultStore.visibleTo(entry.scheduledResult, scheduledResultOwnerId)) return null;
|
|
166
167
|
return entry;
|
|
167
168
|
}
|
|
168
169
|
|
|
@@ -212,8 +213,7 @@ function createProjectLogsStore(opts) {
|
|
|
212
213
|
return read(refFor(id), false);
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
// Build the complete snapshot a revision will store
|
|
216
|
-
// plus the requested changes. Every new canonical edit is self-contained.
|
|
216
|
+
// Build the complete snapshot a revision will store from current state.
|
|
217
217
|
function nextSnapshot(current, validated, data) {
|
|
218
218
|
var snapshot = logsSnapshot.cloneSnapshot(current);
|
|
219
219
|
if (validated.category !== undefined) snapshot.category = validated.category;
|
|
@@ -251,7 +251,6 @@ function createProjectLogsStore(opts) {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// One append per canonical revision, always carrying a complete snapshot.
|
|
254
|
-
// Legacy field names ride along so an older build can still read the record.
|
|
255
254
|
function appendRevision(rootId, op, snapshot, author, extra) {
|
|
256
255
|
var record = {
|
|
257
256
|
id: recordStore.newRecordId(),
|
|
@@ -275,9 +274,7 @@ function createProjectLogsStore(opts) {
|
|
|
275
274
|
return record;
|
|
276
275
|
}
|
|
277
276
|
|
|
278
|
-
// Human participation
|
|
279
|
-
// never a canonical revision. The author is supplied by the authorization
|
|
280
|
-
// layer, never by the caller's payload.
|
|
277
|
+
// Human participation is append-only and never a canonical revision.
|
|
281
278
|
function comment(ref, input, author, context) {
|
|
282
279
|
var located = requireLive(ref);
|
|
283
280
|
var data = input || {};
|
|
@@ -344,7 +341,7 @@ function createProjectLogsStore(opts) {
|
|
|
344
341
|
function categories(options) {
|
|
345
342
|
var seen = {};
|
|
346
343
|
var out = [];
|
|
347
|
-
var live = entries(false);
|
|
344
|
+
var live = entries(false, options && options.scheduledResultOwnerId);
|
|
348
345
|
for (var i = 0; i < live.length; i++) {
|
|
349
346
|
if (!logsQuery.matchesContext(live[i], options || {})) continue;
|
|
350
347
|
var value = live[i].category;
|
|
@@ -379,7 +376,7 @@ function createProjectLogsStore(opts) {
|
|
|
379
376
|
var limit = Number(options2.limit);
|
|
380
377
|
if (!Number.isFinite(limit)) limit = MAX_FEEDBACK_ITEMS;
|
|
381
378
|
limit = Math.max(1, Math.min(MAX_FEEDBACK_ITEMS, Math.floor(limit)));
|
|
382
|
-
var live = entries(false);
|
|
379
|
+
var live = entries(false, options2.scheduledResultOwnerId);
|
|
383
380
|
var out = [];
|
|
384
381
|
var total = 0;
|
|
385
382
|
for (var i = 0; i < live.length; i++) {
|
|
@@ -409,13 +406,13 @@ function createProjectLogsStore(opts) {
|
|
|
409
406
|
|
|
410
407
|
function list(args) {
|
|
411
408
|
var options2 = args || {};
|
|
412
|
-
var result = logsQuery.listEntries(entries(options2.includeDeleted === true), options2, cleanLine, validateKind);
|
|
409
|
+
var result = logsQuery.listEntries(entries(options2.includeDeleted === true, options2.scheduledResultOwnerId), options2, cleanLine, validateKind);
|
|
413
410
|
result.categories = categories(options2);
|
|
414
411
|
return result;
|
|
415
412
|
}
|
|
416
413
|
|
|
417
414
|
function search(args) {
|
|
418
|
-
var result = logsQuery.searchEntries(entries(false), args || {}, cleanLine, validateKind);
|
|
415
|
+
var result = logsQuery.searchEntries(entries(false, args && args.scheduledResultOwnerId), args || {}, cleanLine, validateKind);
|
|
419
416
|
result.categories = categories(args || {});
|
|
420
417
|
return result;
|
|
421
418
|
}
|
|
@@ -457,6 +454,8 @@ function createProjectLogsStore(opts) {
|
|
|
457
454
|
};
|
|
458
455
|
}
|
|
459
456
|
|
|
457
|
+
var scheduledResults = scheduledResultStore.attachScheduledResultStore({ store: store, scopeId: scopeId, refFor: refFor, read: read, entries: entries, findChain: findChain, matchesContext: logsQuery.matchesContext });
|
|
458
|
+
|
|
460
459
|
return {
|
|
461
460
|
root: root,
|
|
462
461
|
scopeId: scopeId,
|
|
@@ -477,6 +476,9 @@ function createProjectLogsStore(opts) {
|
|
|
477
476
|
search: search,
|
|
478
477
|
history: history,
|
|
479
478
|
stats: stats,
|
|
479
|
+
createScheduledResult: scheduledResults.create,
|
|
480
|
+
unreadScheduledResults: scheduledResults.unread,
|
|
481
|
+
markScheduledResultRead: scheduledResults.markRead,
|
|
480
482
|
};
|
|
481
483
|
}
|
|
482
484
|
|
package/lib/project-logs.js
CHANGED
|
@@ -72,6 +72,8 @@ var HANDLED_TYPES = [
|
|
|
72
72
|
"project_log_read",
|
|
73
73
|
"project_log_comment",
|
|
74
74
|
"project_log_delete",
|
|
75
|
+
"scheduled_task_result_mark_read",
|
|
76
|
+
"scheduled_task_result_open_session",
|
|
75
77
|
// Retired human mutations. Still claimed so an older client gets an
|
|
76
78
|
// explicit refusal rather than silence.
|
|
77
79
|
"project_log_create",
|
|
@@ -155,6 +157,31 @@ function attachProjectLogs(ctx) {
|
|
|
155
157
|
return service.bindUser({ projectSlug: slug, user: (ws && ws._clayUser) || null });
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
function scheduledResultState(bound, autoFocus) {
|
|
161
|
+
var state = bound.scheduledResultsState();
|
|
162
|
+
return { type: "scheduled_task_results_state", unreadCount: state.count, newestUnreadRef: state.newestRef, autoFocus: autoFocus === true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sendScheduledResultState(ws) {
|
|
166
|
+
if (!ws || ws._clayPane) return false;
|
|
167
|
+
var bound = userBinding(ws);
|
|
168
|
+
if (!bound) return false;
|
|
169
|
+
try { sendTo(ws, scheduledResultState(bound, true)); return true; } catch (e) { return false; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function broadcastScheduledResultState(sourceWs) {
|
|
173
|
+
var sourceUserId = sourceWs && sourceWs._clayUser ? sourceWs._clayUser.id : null;
|
|
174
|
+
var clients = getClients();
|
|
175
|
+
for (var client of clients) {
|
|
176
|
+
if (!client || client.readyState !== 1 || client._clayPane) continue;
|
|
177
|
+
var clientUserId = client._clayUser ? client._clayUser.id : null;
|
|
178
|
+
if (clientUserId !== sourceUserId) continue;
|
|
179
|
+
var bound = userBinding(client);
|
|
180
|
+
if (!bound) continue;
|
|
181
|
+
try { sendTo(client, scheduledResultState(bound)); } catch (e) {}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
158
185
|
// --- WebSocket surface -----------------------------------------------
|
|
159
186
|
|
|
160
187
|
function fail(ws, requestId, message) {
|
|
@@ -253,6 +280,27 @@ function attachProjectLogs(ctx) {
|
|
|
253
280
|
sendTo(ws, { type: "project_log_commented", requestId: requestId, entry: commented, reviewQueued: reviewQueued });
|
|
254
281
|
return true;
|
|
255
282
|
}
|
|
283
|
+
|
|
284
|
+
if (msg.type === "scheduled_task_result_mark_read") {
|
|
285
|
+
bound.markScheduledResultRead({ ref: msg.ref });
|
|
286
|
+
broadcastScheduledResultState(ws);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (msg.type === "scheduled_task_result_open_session") {
|
|
291
|
+
var resultEntry = bound.readLog({ ref: msg.ref });
|
|
292
|
+
var resultMeta = resultEntry && resultEntry.scheduledResult;
|
|
293
|
+
if (!resultMeta) throw new Error("Scheduled result not found.");
|
|
294
|
+
var target = null;
|
|
295
|
+
sm.sessions.forEach(function (session) {
|
|
296
|
+
if (target || !session || session.sessionOriginId !== resultMeta.driverOriginId) return;
|
|
297
|
+
var marker = session.scheduledTaskRun;
|
|
298
|
+
if ((session.ownerId || null) === (resultMeta.ownerId || null) && marker && marker.role === "driver" && marker.runId === resultMeta.runId && marker.scheduleId === resultMeta.scheduleId) target = session;
|
|
299
|
+
});
|
|
300
|
+
if (!target) throw new Error("This scheduled session is no longer available.");
|
|
301
|
+
sendTo(ws, { type: "scheduled_task_result_session", requestId: requestId, ref: resultEntry.ref, sessionId: target.localId });
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
256
304
|
} catch (e) {
|
|
257
305
|
fail(ws, requestId, (e && e.message) || "Project Logs could not complete the request.");
|
|
258
306
|
return true;
|
|
@@ -378,6 +426,26 @@ function attachProjectLogs(ctx) {
|
|
|
378
426
|
"\n" + logsMcp.REVIEW_CONTRACT + pendingFeedbackSignal(session);
|
|
379
427
|
}
|
|
380
428
|
|
|
429
|
+
function recordScheduledResult(data) {
|
|
430
|
+
if (!service || isMate) return null;
|
|
431
|
+
var result = service.recordScheduledResult(Object.assign({}, data || {}, { projectSlug: slug }));
|
|
432
|
+
if (!result || !result.entry || !result.created) return result;
|
|
433
|
+
var clients = getClients();
|
|
434
|
+
for (var client of clients) {
|
|
435
|
+
if (!client || client.readyState !== 1 || client._clayPane) continue;
|
|
436
|
+
if (result.entry.scheduledResult.ownerId && (!client._clayUser || client._clayUser.id !== result.entry.scheduledResult.ownerId)) continue;
|
|
437
|
+
var bound = userBinding(client);
|
|
438
|
+
if (!bound) continue;
|
|
439
|
+
try { bound.readLog({ ref: result.entry.ref }); } catch (e) { continue; }
|
|
440
|
+
sendTo(client, {
|
|
441
|
+
type: "scheduled_task_result_created", ref: result.entry.ref,
|
|
442
|
+
title: result.entry.title, summary: result.entry.summary,
|
|
443
|
+
outcome: result.entry.scheduledResult.outcome,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
|
|
381
449
|
return {
|
|
382
450
|
handleLogsMessage: handleLogsMessage,
|
|
383
451
|
createMcpServer: createMcpServer,
|
|
@@ -386,6 +454,8 @@ function attachProjectLogs(ctx) {
|
|
|
386
454
|
getBridgeTools: getBridgeTools,
|
|
387
455
|
callBridgeTool: callBridgeTool,
|
|
388
456
|
getSystemPrompt: getSystemPrompt,
|
|
457
|
+
recordScheduledResult: recordScheduledResult,
|
|
458
|
+
sendScheduledResultState: sendScheduledResultState,
|
|
389
459
|
};
|
|
390
460
|
}
|
|
391
461
|
|
package/lib/project-loop.js
CHANGED
|
@@ -373,6 +373,7 @@ function attachLoop(ctx) {
|
|
|
373
373
|
if (durableSchedule) durableSchedule.sync();
|
|
374
374
|
send({ type: "loop_registry_updated", records: getHubSchedules() });
|
|
375
375
|
},
|
|
376
|
+
onRunTerminal: ctx.onScheduledRunTerminal,
|
|
376
377
|
});
|
|
377
378
|
loopRegistry.load();
|
|
378
379
|
if (ctx.durableScheduler) {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Server-owned Scheduled Tasks results inside the Project Logs record stream.
|
|
2
|
+
// This capability is never exposed through the human or agent write surfaces.
|
|
3
|
+
|
|
4
|
+
var crypto = require("crypto");
|
|
5
|
+
var recordStore = require("./knowledge-record-store");
|
|
6
|
+
var logsSchema = require("./project-logs-schema");
|
|
7
|
+
var logsSnapshot = require("./project-logs-snapshot");
|
|
8
|
+
|
|
9
|
+
var ID_PATTERN = /^[A-Za-z0-9_-]{1,200}$/;
|
|
10
|
+
var OUTCOMES = ["completed", "failed", "needs-input", "interrupted", "skipped-busy"];
|
|
11
|
+
|
|
12
|
+
function clean(value, max) {
|
|
13
|
+
return logsSchema.cleanLine(typeof value === "string" ? value : "", max);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeMetadata(value) {
|
|
17
|
+
var source = value || {};
|
|
18
|
+
if (!ID_PATTERN.test(source.runId || "") || !ID_PATTERN.test(source.scheduleId || "")) return null;
|
|
19
|
+
if (OUTCOMES.indexOf(source.outcome) === -1) return null;
|
|
20
|
+
return {
|
|
21
|
+
runId: source.runId,
|
|
22
|
+
scheduleId: source.scheduleId,
|
|
23
|
+
outcome: source.outcome,
|
|
24
|
+
ownerId: source.ownerId ? clean(String(source.ownerId), 120) : null,
|
|
25
|
+
driverOriginId: clean(source.driverOriginId || "", 200) || null,
|
|
26
|
+
workerOriginId: clean(source.workerOriginId || "", 200) || null,
|
|
27
|
+
driverProviderSessionId: clean(source.driverProviderSessionId || "", 200) || null,
|
|
28
|
+
workerProviderSessionId: clean(source.workerProviderSessionId || "", 200) || null,
|
|
29
|
+
startedAt: Number(source.startedAt) || null,
|
|
30
|
+
finishedAt: Number(source.finishedAt) || null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function visibleTo(metadata, viewerId) {
|
|
35
|
+
if (!metadata || viewerId === undefined) return true;
|
|
36
|
+
return (metadata.ownerId || null) === (viewerId || null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function outcomeLabel(outcome) {
|
|
40
|
+
if (outcome === "completed") return "Completed";
|
|
41
|
+
if (outcome === "needs-input") return "Needs input";
|
|
42
|
+
if (outcome === "interrupted") return "Interrupted";
|
|
43
|
+
if (outcome === "skipped-busy") return "Skipped";
|
|
44
|
+
return "Failed";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fallbackSummary(outcome) {
|
|
48
|
+
if (outcome === "completed") return "The scheduled task completed successfully.";
|
|
49
|
+
if (outcome === "needs-input") return "The scheduled task is waiting for user input or permission.";
|
|
50
|
+
if (outcome === "interrupted") return "The scheduled task was interrupted before completion.";
|
|
51
|
+
if (outcome === "skipped-busy") return "The scheduled occurrence was skipped because an earlier run was still active.";
|
|
52
|
+
return "The scheduled task failed.";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rootId(scopeId, runId) {
|
|
56
|
+
var digest = crypto.createHash("sha256").update(scopeId + "\u0000scheduled-result\u0000" + runId).digest("base64url");
|
|
57
|
+
return "sresult-" + digest.substring(0, 24);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function attachScheduledResultStore(ctx) {
|
|
61
|
+
function resultRecord(runId) {
|
|
62
|
+
var records = ctx.store.all();
|
|
63
|
+
for (var i = 0; i < records.length; i++) {
|
|
64
|
+
if (records[i].op === "create" && records[i].scheduledResult && records[i].scheduledResult.runId === runId) return records[i];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function create(input, context) {
|
|
70
|
+
var data = input || {};
|
|
71
|
+
var metadata = normalizeMetadata(data);
|
|
72
|
+
if (!metadata) throw new Error("Scheduled result identity is invalid.");
|
|
73
|
+
var existing = resultRecord(metadata.runId);
|
|
74
|
+
if (existing) return { entry: ctx.read(ctx.refFor(existing.rootId || existing.id), false), created: false };
|
|
75
|
+
var name = clean(data.name || "Scheduled task", 160) || "Scheduled task";
|
|
76
|
+
var reason = clean(data.summary || data.reason || "", 400);
|
|
77
|
+
var summary = reason || fallbackSummary(metadata.outcome);
|
|
78
|
+
var title = name + " · " + outcomeLabel(metadata.outcome);
|
|
79
|
+
var body = "## Scheduled result\n\n" + summary + "\n\n" +
|
|
80
|
+
"- Outcome: " + outcomeLabel(metadata.outcome) + "\n" +
|
|
81
|
+
"- Run: `" + metadata.runId + "`\n" +
|
|
82
|
+
(metadata.startedAt ? "- Started: " + new Date(metadata.startedAt).toISOString() + "\n" : "") +
|
|
83
|
+
(metadata.finishedAt ? "- Finished: " + new Date(metadata.finishedAt).toISOString() + "\n" : "");
|
|
84
|
+
var id = rootId(ctx.scopeId, metadata.runId);
|
|
85
|
+
var snapshot = logsSnapshot.cloneSnapshot({ category: "scheduled-result", priority: metadata.outcome === "failed" ? "important" : "normal", title: title, summary: summary, body: body, tags: ["scheduled-task", metadata.outcome], links: [] });
|
|
86
|
+
ctx.store.append({
|
|
87
|
+
id: id, rootId: id, op: "create", scope: ctx.scopeId,
|
|
88
|
+
kind: snapshot.category, priority: snapshot.priority, title: snapshot.title,
|
|
89
|
+
summary: snapshot.summary, body: snapshot.body, tags: snapshot.tags,
|
|
90
|
+
links: snapshot.links, snapshot: snapshot,
|
|
91
|
+
author: logsSchema.normalizeAuthor({ type: "system", displayName: "Scheduled Tasks" }),
|
|
92
|
+
context: context || null, scheduledResult: metadata, at: metadata.finishedAt || Date.now(),
|
|
93
|
+
});
|
|
94
|
+
return { entry: ctx.read(ctx.refFor(id), false), created: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasRead(root, readerId) {
|
|
98
|
+
var records = ctx.store.all();
|
|
99
|
+
for (var i = 0; i < records.length; i++) {
|
|
100
|
+
if (records[i].rootId === root && records[i].op === "scheduled-result-read" && (records[i].readerId || null) === (readerId || null)) return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function unread(viewerId, options) {
|
|
106
|
+
var entries = ctx.entries(false, viewerId);
|
|
107
|
+
var refs = [];
|
|
108
|
+
for (var i = 0; i < entries.length; i++) {
|
|
109
|
+
if (!entries[i].scheduledResult) continue;
|
|
110
|
+
if (ctx.matchesContext && !ctx.matchesContext(entries[i], options || {})) continue;
|
|
111
|
+
var located = ctx.findChain(entries[i].ref);
|
|
112
|
+
if (located && !hasRead(located.rootId, viewerId)) refs.push(entries[i].ref);
|
|
113
|
+
}
|
|
114
|
+
return { count: refs.length, newestRef: refs.length ? refs[0] : null, refs: refs };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function markRead(ref, viewerId, options) {
|
|
118
|
+
var entry = ctx.read(ref, false, viewerId);
|
|
119
|
+
if (!entry || !entry.scheduledResult) throw new Error("Scheduled result not found.");
|
|
120
|
+
var located = ctx.findChain(ref);
|
|
121
|
+
if (!located) throw new Error("Scheduled result not found.");
|
|
122
|
+
if (!hasRead(located.rootId, viewerId)) {
|
|
123
|
+
ctx.store.append({ id: recordStore.newRecordId(), rootId: located.rootId, op: "scheduled-result-read", scope: ctx.scopeId, readerId: viewerId || null, at: Date.now() });
|
|
124
|
+
}
|
|
125
|
+
return unread(viewerId, options);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { create: create, markRead: markRead, unread: unread };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { attachScheduledResultStore: attachScheduledResultStore, normalizeMetadata: normalizeMetadata, visibleTo: visibleTo };
|
|
@@ -50,8 +50,9 @@ function attachScheduledTaskExecution(ctx) {
|
|
|
50
50
|
var active = record.activeRun;
|
|
51
51
|
var driver = sm.sessions.get(active.driverSessionId);
|
|
52
52
|
var worker = sm.sessions.get(active.workerSessionId);
|
|
53
|
+
var finalSummary = clean(reason, 1000) || null;
|
|
53
54
|
var saved = registry.completeRun(recordId, runId, {
|
|
54
|
-
outcome: outcome,
|
|
55
|
+
outcome: outcome, summary: finalSummary, reason: finalSummary,
|
|
55
56
|
driverProviderSessionId: driver && driver.cliSessionId || active.driverProviderSessionId || null,
|
|
56
57
|
workerProviderSessionId: worker && worker.cliSessionId || active.workerProviderSessionId || null,
|
|
57
58
|
});
|
|
@@ -65,6 +66,37 @@ function attachScheduledTaskExecution(ctx) {
|
|
|
65
66
|
return true;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
function recordTerminal(record, run, attempt) {
|
|
70
|
+
if (!record || !run || !run.runId || !run.outcome || typeof ctx.recordResult !== "function") return false;
|
|
71
|
+
try {
|
|
72
|
+
ctx.recordResult({
|
|
73
|
+
scheduleId: record.id, runId: run.runId, name: record.name || "Scheduled task",
|
|
74
|
+
ownerId: record.ownerId || null, outcome: run.outcome,
|
|
75
|
+
summary: run.summary || run.reason || null, startedAt: run.startedAt || null,
|
|
76
|
+
finishedAt: run.finishedAt || Date.now(), driverOriginId: run.driverOriginId || null,
|
|
77
|
+
workerOriginId: run.workerOriginId || null,
|
|
78
|
+
driverProviderSessionId: run.driverProviderSessionId || null,
|
|
79
|
+
workerProviderSessionId: run.workerProviderSessionId || null,
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("[scheduled-task] Failed to record result for " + run.runId + ":", error.message || error);
|
|
84
|
+
if (Number(attempt || 0) < 2) setTimeout(function () { recordTerminal(record, run, Number(attempt || 0) + 1); }, 1000);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function syncTerminalResults() {
|
|
90
|
+
var records = registry.getAll ? registry.getAll() : [];
|
|
91
|
+
for (var i = 0; i < records.length; i++) {
|
|
92
|
+
if (records[i].createdViaScheduledTasks !== true && records[i].source !== "schedule") continue;
|
|
93
|
+
var runs = Array.isArray(records[i].runs) ? records[i].runs : [];
|
|
94
|
+
for (var j = 0; j < runs.length; j++) {
|
|
95
|
+
if (runs[j] && runs[j].runId && runs[j].outcome) recordTerminal(records[i], runs[j]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
function failStart(recordId, runId, created, error, preDispatch) {
|
|
69
101
|
if (preDispatch) rollbackPair(created);
|
|
70
102
|
else if (created) {
|
|
@@ -110,6 +142,7 @@ function attachScheduledTaskExecution(ctx) {
|
|
|
110
142
|
name: record.name || "Scheduled task",
|
|
111
143
|
driver: record.execution.driver,
|
|
112
144
|
worker: record.execution.worker,
|
|
145
|
+
hidden: true,
|
|
113
146
|
});
|
|
114
147
|
var runState = {
|
|
115
148
|
status: "running", groupId: created.group.id,
|
|
@@ -230,7 +263,7 @@ function attachScheduledTaskExecution(ctx) {
|
|
|
230
263
|
return driver ? stopForSession(driver, reason) : finish(record.id, record.activeRun.runId, "interrupted", reason, false);
|
|
231
264
|
}
|
|
232
265
|
|
|
233
|
-
return { getToolDefs: getToolDefs, onPartnerResult: onPartnerResult, onTurnDone: onTurnDone, prepare: prepare, runnable: runnable, stopForSession: stopForSession, stopRecord: stopRecord, trigger: trigger };
|
|
266
|
+
return { getToolDefs: getToolDefs, onPartnerResult: onPartnerResult, onTurnDone: onTurnDone, prepare: prepare, recordTerminal: recordTerminal, runnable: runnable, stopForSession: stopForSession, stopRecord: stopRecord, syncTerminalResults: syncTerminalResults, trigger: trigger };
|
|
234
267
|
}
|
|
235
268
|
|
|
236
269
|
module.exports = { attachScheduledTaskExecution: attachScheduledTaskExecution, validRuntime: validRuntime };
|
package/lib/project.js
CHANGED
|
@@ -1422,6 +1422,9 @@ function createProjectContext(opts) {
|
|
|
1422
1422
|
canExecuteScheduledTask: function (record) {
|
|
1423
1423
|
return !!(_scheduledTaskExecution && _scheduledTaskExecution.runnable(record).ok);
|
|
1424
1424
|
},
|
|
1425
|
+
onScheduledRunTerminal: function (record, run) {
|
|
1426
|
+
if (_scheduledTaskExecution) _scheduledTaskExecution.recordTerminal(record, run);
|
|
1427
|
+
},
|
|
1425
1428
|
onProcessingChanged: onProcessingChanged,
|
|
1426
1429
|
hydrateImageRefs: hydrateImageRefs,
|
|
1427
1430
|
});
|
|
@@ -1437,6 +1440,7 @@ function createProjectContext(opts) {
|
|
|
1437
1440
|
getSdk: function () { return sdk; }, getLinuxUserForSession: getLinuxUserForSession,
|
|
1438
1441
|
requiresLinuxUser: function () { return osUsers; },
|
|
1439
1442
|
onProcessingChanged: onProcessingChanged,
|
|
1443
|
+
recordResult: function (result) { return _projectLogs.recordScheduledResult(result); },
|
|
1440
1444
|
authorize: function (record) {
|
|
1441
1445
|
return authorizeStoredOwner(record, {
|
|
1442
1446
|
usersModule: usersModule, osUsers: osUsers,
|
|
@@ -1446,6 +1450,7 @@ function createProjectContext(opts) {
|
|
|
1446
1450
|
});
|
|
1447
1451
|
},
|
|
1448
1452
|
});
|
|
1453
|
+
setTimeout(function () { _scheduledTaskExecution.syncTerminalResults(); }, 0);
|
|
1449
1454
|
|
|
1450
1455
|
_scheduledTasks = attachScheduledTasks({
|
|
1451
1456
|
cwd: cwd,
|
|
@@ -1940,6 +1945,7 @@ function createProjectContext(opts) {
|
|
|
1940
1945
|
saveContextSources: saveContextSources,
|
|
1941
1946
|
_email: _email,
|
|
1942
1947
|
_notifications: _notifications,
|
|
1948
|
+
_projectLogs: _projectLogs,
|
|
1943
1949
|
});
|
|
1944
1950
|
|
|
1945
1951
|
var _models = attachModels({
|
package/lib/public/app.js
CHANGED
|
@@ -381,6 +381,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
381
381
|
projectLogsCanDelete: false,
|
|
382
382
|
projectLogsUnread: 0,
|
|
383
383
|
projectLogsSeenRevisions: {},
|
|
384
|
+
scheduledResultUnread: 0,
|
|
385
|
+
scheduledResultAutoFocusHandled: false,
|
|
386
|
+
scheduledResultFocusRef: null,
|
|
387
|
+
scheduledResultPendingReadRef: null,
|
|
388
|
+
scheduledResultSeenRefs: {},
|
|
389
|
+
scheduledResultSessionRequestId: null,
|
|
384
390
|
issuesOpen: false,
|
|
385
391
|
issuesView: "list",
|
|
386
392
|
issuesSelected: null,
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
margin-top: 8px;
|
|
182
182
|
}
|
|
183
183
|
.notif-banner-allow, .notif-banner-always, .notif-banner-deny, .notif-banner-goto,
|
|
184
|
-
.notif-banner-tui-open, .notif-banner-tui-goto {
|
|
184
|
+
.notif-banner-tui-open, .notif-banner-tui-goto, .notif-banner-result-open {
|
|
185
185
|
padding: 4px 10px;
|
|
186
186
|
border-radius: 6px;
|
|
187
187
|
font-size: 11px;
|
|
@@ -191,11 +191,11 @@
|
|
|
191
191
|
border: none;
|
|
192
192
|
transition: opacity 0.15s, background 0.15s;
|
|
193
193
|
}
|
|
194
|
-
.notif-banner-tui-open {
|
|
194
|
+
.notif-banner-tui-open, .notif-banner-result-open {
|
|
195
195
|
background: var(--accent, #6c5ce7);
|
|
196
196
|
color: #fff;
|
|
197
197
|
}
|
|
198
|
-
.notif-banner-tui-open:hover { opacity: 0.85; }
|
|
198
|
+
.notif-banner-tui-open:hover, .notif-banner-result-open:hover { opacity: 0.85; }
|
|
199
199
|
.notif-banner-tui-goto {
|
|
200
200
|
background: var(--accent2);
|
|
201
201
|
border: none;
|
|
@@ -196,3 +196,18 @@
|
|
|
196
196
|
.project-log-row { padding: 14px 14px 13px; }
|
|
197
197
|
.project-log-document { padding: 24px 18px 38px; }
|
|
198
198
|
}
|
|
199
|
+
.project-log-session-button {
|
|
200
|
+
align-self: flex-start;
|
|
201
|
+
display: inline-flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: 6px;
|
|
204
|
+
margin: 0 0 16px;
|
|
205
|
+
border: 1px solid var(--border);
|
|
206
|
+
border-radius: 8px;
|
|
207
|
+
padding: 7px 10px;
|
|
208
|
+
background: var(--bg-alt);
|
|
209
|
+
color: var(--text);
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.project-log-session-button:hover { border-color: var(--accent); }
|
|
@@ -44,7 +44,7 @@ import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTerm
|
|
|
44
44
|
import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult } from './context-sources.js';
|
|
45
45
|
import { refreshEmailSettings } from './user-settings.js';
|
|
46
46
|
import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, handleNoteWritten } from './sticky-notes.js';
|
|
47
|
-
import { handleProjectLogsState, handleProjectLogEntry, handleProjectLogCommented, handleProjectLogCommentReviewed, handleProjectLogDeleted, handleProjectLogUpdated, handleProjectLogsError } from './project-logs.js';
|
|
47
|
+
import { handleProjectLogsState, handleProjectLogEntry, handleProjectLogCommented, handleProjectLogCommentReviewed, handleProjectLogDeleted, handleProjectLogUpdated, handleProjectLogsError, handleScheduledTaskResultsState, handleScheduledTaskResultCreated, handleScheduledTaskResultSession } from './project-logs.js';
|
|
48
48
|
import { handleSkillInstalled, handleSkillUninstalled } from './skills.js';
|
|
49
49
|
import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearPendingRewindUuid, addRewindButton } from './rewind.js';
|
|
50
50
|
import { checkAdminAccess } from './admin.js';
|
|
@@ -1664,6 +1664,18 @@ export function processMessage(msg) {
|
|
|
1664
1664
|
handleProjectLogsError(msg);
|
|
1665
1665
|
break;
|
|
1666
1666
|
|
|
1667
|
+
case "scheduled_task_results_state":
|
|
1668
|
+
handleScheduledTaskResultsState(msg);
|
|
1669
|
+
break;
|
|
1670
|
+
|
|
1671
|
+
case "scheduled_task_result_created":
|
|
1672
|
+
handleScheduledTaskResultCreated(msg);
|
|
1673
|
+
break;
|
|
1674
|
+
|
|
1675
|
+
case "scheduled_task_result_session":
|
|
1676
|
+
handleScheduledTaskResultSession(msg);
|
|
1677
|
+
break;
|
|
1678
|
+
|
|
1667
1679
|
case "process_stats":
|
|
1668
1680
|
updateStatusPanel(msg);
|
|
1669
1681
|
updateSettingsStats(msg);
|
|
@@ -45,6 +45,7 @@ function actorLabel(actor) {
|
|
|
45
45
|
// only the account the session ran under.
|
|
46
46
|
function authorLine(entry) {
|
|
47
47
|
var actor = (entry && (entry.updatedBy || entry.createdBy)) || {};
|
|
48
|
+
if (actor.type === "system") return actor.displayName || "Scheduled Tasks";
|
|
48
49
|
if (actor.type === "session") {
|
|
49
50
|
return actor.vendor ? "Project Driver (" + vendorLabel(actor.vendor) + ")" : "Project Driver";
|
|
50
51
|
}
|
|
@@ -363,7 +364,7 @@ export function renderDetail(detailEl, entry, handlers) {
|
|
|
363
364
|
formatTime(entry.updatedAt || entry.createdAt),
|
|
364
365
|
].filter(Boolean).join(" · ");
|
|
365
366
|
setText(header, ".project-log-byline", byline);
|
|
366
|
-
if (handlers && handlers.canDelete) {
|
|
367
|
+
if (handlers && handlers.canDelete && !entry.scheduledResult) {
|
|
367
368
|
var deleteButton = document.createElement("button");
|
|
368
369
|
deleteButton.type = "button";
|
|
369
370
|
deleteButton.className = "project-log-delete-button";
|
|
@@ -373,6 +374,15 @@ export function renderDetail(detailEl, entry, handlers) {
|
|
|
373
374
|
}
|
|
374
375
|
article.appendChild(header);
|
|
375
376
|
|
|
377
|
+
if (entry.scheduledResult && entry.scheduledResult.driverOriginId && handlers && handlers.onOpenSession) {
|
|
378
|
+
var sessionButton = document.createElement("button");
|
|
379
|
+
sessionButton.type = "button";
|
|
380
|
+
sessionButton.className = "project-log-session-button";
|
|
381
|
+
sessionButton.innerHTML = '<i data-lucide="external-link"></i><span>View session</span>';
|
|
382
|
+
sessionButton.addEventListener("click", function () { handlers.onOpenSession(entry); });
|
|
383
|
+
article.appendChild(sessionButton);
|
|
384
|
+
}
|
|
385
|
+
|
|
376
386
|
var markdown = document.createElement("div");
|
|
377
387
|
markdown.className = "project-log-markdown md-content";
|
|
378
388
|
markdown.innerHTML = renderMarkdown(entry.body || "");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Project Logs — the project's ledger of agent-authored decisions and work.
|
|
2
|
-
//
|
|
3
2
|
// A bounded right-side workbench pane, like the document viewer and terminal.
|
|
4
3
|
// Inside it there is one navigation stack rather than a master/detail split:
|
|
5
4
|
// the list fills the pane, opening an entry replaces it with the full-pane
|
|
@@ -14,6 +13,9 @@ import { store } from './store.js';
|
|
|
14
13
|
import { getWs } from './ws-ref.js';
|
|
15
14
|
import { refreshIcons } from './icons.js';
|
|
16
15
|
import { showToast } from './utils.js';
|
|
16
|
+
import { showScheduledResultNotification } from './scheduled-result-notification.js';
|
|
17
|
+
import * as scheduledResultClient from './scheduled-result-client.js';
|
|
18
|
+
import { clearPendingScheduledResultRead, handleScheduledResultDisplayed, initScheduledResultDisplay, openScheduledResultSession } from './scheduled-result-display.js';
|
|
17
19
|
import { closeScheduledTasks } from './scheduled-tasks.js';
|
|
18
20
|
import { closeNotesBrowser } from './sticky-notes-browser.js';
|
|
19
21
|
import { closeFileViewer } from './filebrowser.js';
|
|
@@ -35,7 +37,7 @@ var requestCounter = 0;
|
|
|
35
37
|
function syncUnreadBadge() {
|
|
36
38
|
var button = document.getElementById("project-logs-btn");
|
|
37
39
|
if (!button) return;
|
|
38
|
-
var unread = store.get('projectLogsUnread') || 0;
|
|
40
|
+
var unread = (store.get('projectLogsUnread') || 0) + (store.get('scheduledResultUnread') || 0);
|
|
39
41
|
button.classList.toggle("project-logs-unread", unread > 0);
|
|
40
42
|
var badge = document.getElementById("project-logs-count");
|
|
41
43
|
if (!badge) return;
|
|
@@ -164,10 +166,10 @@ function applyWindowState(wide, fullscreen) {
|
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
// --- Navigation stack ----------------------------------------------------
|
|
167
|
-
|
|
168
169
|
function showList() {
|
|
169
170
|
if (!panel) return;
|
|
170
171
|
store.set({ projectLogsView: "list", projectLogsSelectedRef: null });
|
|
172
|
+
clearPendingScheduledResultRead();
|
|
171
173
|
detailEl.classList.add("hidden");
|
|
172
174
|
listEl.classList.remove("hidden");
|
|
173
175
|
panel.querySelector("#project-logs-toolbar").classList.remove("hidden");
|
|
@@ -188,8 +190,10 @@ function showDetail(entry) {
|
|
|
188
190
|
renderDetail(detailEl, entry, {
|
|
189
191
|
onComment: submitComment,
|
|
190
192
|
onDelete: openDeleteDialog,
|
|
193
|
+
onOpenSession: openScheduledResultSession,
|
|
191
194
|
canDelete: store.get('projectLogsCanDelete') === true,
|
|
192
195
|
});
|
|
196
|
+
handleScheduledResultDisplayed(entry);
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
function openDeleteDialog(entry) {
|
|
@@ -230,7 +234,6 @@ function confirmDelete() {
|
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
// --- Requests ------------------------------------------------------------
|
|
233
|
-
|
|
234
237
|
function requestList() {
|
|
235
238
|
var requestId = nextRequestId("list-logs");
|
|
236
239
|
store.set({ projectLogsListRequestId: requestId });
|
|
@@ -288,8 +291,10 @@ export function initProjectLogs() {
|
|
|
288
291
|
if (store.get('projectLogsOpen')) closeProjectLogs();
|
|
289
292
|
else openProjectLogs();
|
|
290
293
|
});
|
|
294
|
+
initScheduledResultDisplay();
|
|
291
295
|
syncUnreadBadge();
|
|
292
296
|
store.subscribe(function (state, previous) {
|
|
297
|
+
if (state.connected === false && previous.connected === true) store.set({ scheduledResultAutoFocusHandled: false });
|
|
293
298
|
if (state.currentSlug === previous.currentSlug) return;
|
|
294
299
|
closeProjectLogs();
|
|
295
300
|
applyWindowState(false, false);
|
|
@@ -309,7 +314,13 @@ export function initProjectLogs() {
|
|
|
309
314
|
projectLogsDeleteRequestId: null,
|
|
310
315
|
projectLogsCanDelete: false,
|
|
311
316
|
projectLogsUnread: 0,
|
|
312
|
-
projectLogsSeenRevisions: {}
|
|
317
|
+
projectLogsSeenRevisions: {},
|
|
318
|
+
scheduledResultUnread: 0,
|
|
319
|
+
scheduledResultAutoFocusHandled: false,
|
|
320
|
+
scheduledResultFocusRef: null,
|
|
321
|
+
scheduledResultPendingReadRef: null,
|
|
322
|
+
scheduledResultSeenRefs: {},
|
|
323
|
+
scheduledResultSessionRequestId: null,
|
|
313
324
|
});
|
|
314
325
|
syncUnreadBadge();
|
|
315
326
|
});
|
|
@@ -384,11 +395,35 @@ export function handleProjectLogsState(msg) {
|
|
|
384
395
|
|
|
385
396
|
export function handleProjectLogEntry(msg) {
|
|
386
397
|
if (msg.requestId && msg.requestId !== store.get('projectLogsReadRequestId')) return;
|
|
387
|
-
if (!msg.entry) return;
|
|
398
|
+
if (!msg.entry || !store.get('projectLogsOpen')) return;
|
|
388
399
|
store.set({ projectLogsCanDelete: msg.canDelete === true });
|
|
389
400
|
showDetail(msg.entry);
|
|
390
401
|
}
|
|
391
402
|
|
|
403
|
+
export function handleScheduledTaskResultsState(msg) {
|
|
404
|
+
var transition = scheduledResultClient.scheduledResultStateTransition(store.snap(), msg);
|
|
405
|
+
store.set(transition.patch); syncUnreadBadge();
|
|
406
|
+
if (!transition.openRef) return false;
|
|
407
|
+
var opened = openProjectLog(transition.openRef);
|
|
408
|
+
if (!opened) store.set({ scheduledResultFocusRef: null });
|
|
409
|
+
return opened;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function handleScheduledTaskResultCreated(msg) {
|
|
413
|
+
var patch = scheduledResultClient.scheduledResultCreatedTransition(store.snap(), msg);
|
|
414
|
+
if (!patch) return false;
|
|
415
|
+
store.set(patch); syncUnreadBadge();
|
|
416
|
+
showScheduledResultNotification(msg.title || "Scheduled task finished", msg.summary || "", function () { openProjectLog(msg.ref); });
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function handleScheduledTaskResultSession(msg) {
|
|
421
|
+
var sessionId = scheduledResultClient.scheduledResultSessionId(store.snap(), msg);
|
|
422
|
+
if (sessionId === null) return false;
|
|
423
|
+
store.set({ scheduledResultSessionRequestId: null });
|
|
424
|
+
return send({ type: "switch_session", id: sessionId });
|
|
425
|
+
}
|
|
426
|
+
|
|
392
427
|
export function handleProjectLogCommented(msg) {
|
|
393
428
|
if (msg.requestId && msg.requestId !== store.get('projectLogsCommentRequestId')) return;
|
|
394
429
|
if (!msg.entry) return;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Pure Scheduled Tasks result-news decisions. The production controller owns
|
|
2
|
+
// store, WebSocket, DOM, and peer-module calls directly.
|
|
3
|
+
|
|
4
|
+
export function scheduledResultStateTransition(state, msg) {
|
|
5
|
+
var patch = { scheduledResultUnread: Number(msg.unreadCount) || 0 };
|
|
6
|
+
var openRef = null;
|
|
7
|
+
if (msg.autoFocus && msg.newestUnreadRef && !state.scheduledResultAutoFocusHandled) {
|
|
8
|
+
patch.scheduledResultAutoFocusHandled = true;
|
|
9
|
+
patch.scheduledResultFocusRef = msg.newestUnreadRef;
|
|
10
|
+
openRef = msg.newestUnreadRef;
|
|
11
|
+
}
|
|
12
|
+
return { patch: patch, openRef: openRef };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function scheduledResultCreatedTransition(state, msg) {
|
|
16
|
+
if (!msg || !msg.ref || (state.scheduledResultSeenRefs || {})[msg.ref]) return null;
|
|
17
|
+
var seen = Object.assign({}, state.scheduledResultSeenRefs || {});
|
|
18
|
+
seen[msg.ref] = true;
|
|
19
|
+
return { scheduledResultSeenRefs: seen, scheduledResultUnread: (state.scheduledResultUnread || 0) + 1 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function shouldAcknowledgeScheduledResult(entry, state, visibilityState) {
|
|
23
|
+
return !!(entry && entry.scheduledResult && visibilityState !== "hidden" && state.projectLogsOpen && state.projectLogsView === "detail" && state.projectLogsSelectedRef === entry.ref);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function shouldFocusScheduledResult(entry, state) {
|
|
27
|
+
return !!(entry && state.scheduledResultFocusRef === entry.ref);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function scheduledResultSessionId(state, msg) {
|
|
31
|
+
if (msg.requestId && msg.requestId !== state.scheduledResultSessionRequestId) return null;
|
|
32
|
+
return typeof msg.sessionId === "number" ? msg.sessionId : null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { showToast } from './utils.js';
|
|
4
|
+
import { shouldAcknowledgeScheduledResult, shouldFocusScheduledResult } from './scheduled-result-client.js';
|
|
5
|
+
|
|
6
|
+
function sendRead(ref) {
|
|
7
|
+
var ws = getWs();
|
|
8
|
+
if (!ws || ws.readyState !== 1) return false;
|
|
9
|
+
ws.send(JSON.stringify({ type: "scheduled_task_result_mark_read", ref: ref }));
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var openRequestCounter = 0;
|
|
14
|
+
export function openScheduledResultSession(entry) {
|
|
15
|
+
if (!entry || !entry.scheduledResult) return false;
|
|
16
|
+
openRequestCounter += 1;
|
|
17
|
+
var requestId = "open-scheduled-session-" + Date.now() + "-" + openRequestCounter;
|
|
18
|
+
store.set({ scheduledResultSessionRequestId: requestId });
|
|
19
|
+
var ws = getWs();
|
|
20
|
+
if (!ws || ws.readyState !== 1) { showToast("Reconnect to view this scheduled session.", "warn"); return false; }
|
|
21
|
+
ws.send(JSON.stringify({ type: "scheduled_task_result_open_session", requestId: requestId, ref: entry.ref })); return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function acknowledgeVisible() {
|
|
25
|
+
var ref = store.get('scheduledResultPendingReadRef');
|
|
26
|
+
if (!ref || document.visibilityState === "hidden") return;
|
|
27
|
+
store.set({ scheduledResultPendingReadRef: null });
|
|
28
|
+
var state = store.snap();
|
|
29
|
+
if (state.projectLogsOpen && state.projectLogsView === "detail" && state.projectLogsSelectedRef === ref) sendRead(ref);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearPendingScheduledResultRead() {
|
|
33
|
+
store.set({ scheduledResultPendingReadRef: null });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function handleScheduledResultDisplayed(entry) {
|
|
37
|
+
var state = store.snap();
|
|
38
|
+
if (shouldFocusScheduledResult(entry, state)) {
|
|
39
|
+
store.set({ scheduledResultFocusRef: null });
|
|
40
|
+
var heading = document.querySelector("#project-logs-detail .project-log-document h1");
|
|
41
|
+
if (heading) { heading.setAttribute("tabindex", "-1"); heading.focus(); }
|
|
42
|
+
}
|
|
43
|
+
if (shouldAcknowledgeScheduledResult(entry, store.snap(), document.visibilityState)) {
|
|
44
|
+
clearPendingScheduledResultRead(); sendRead(entry.ref);
|
|
45
|
+
} else if (entry && entry.scheduledResult && document.visibilityState === "hidden") store.set({ scheduledResultPendingReadRef: entry.ref });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function initScheduledResultDisplay() {
|
|
49
|
+
document.addEventListener("visibilitychange", acknowledgeVisible);
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { refreshIcons, iconHtml } from './icons.js';
|
|
2
|
+
import { escapeHtml } from './utils.js';
|
|
3
|
+
|
|
4
|
+
function removeBanner(banner) {
|
|
5
|
+
if (!banner || !banner.parentNode) return;
|
|
6
|
+
banner.classList.remove("show");
|
|
7
|
+
setTimeout(function () { if (banner.parentNode) banner.remove(); }, 220);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function showScheduledResultNotification(title, body, onOpen) {
|
|
11
|
+
var container = document.querySelector(".notif-banner-container");
|
|
12
|
+
if (!container) return false;
|
|
13
|
+
var banner = document.createElement("div");
|
|
14
|
+
banner.className = "notif-banner";
|
|
15
|
+
banner.innerHTML =
|
|
16
|
+
'<div class="notif-banner-icon">' + iconHtml("calendar-check") + '</div>' +
|
|
17
|
+
'<div class="notif-banner-body"><div class="notif-banner-project">SCHEDULED TASKS</div>' +
|
|
18
|
+
'<div class="notif-banner-title">' + escapeHtml(title || "Scheduled task finished") + '</div>' +
|
|
19
|
+
(body ? '<div class="notif-banner-text">' + escapeHtml(body) + '</div>' : '') +
|
|
20
|
+
'<div class="notif-banner-actions"><button class="notif-banner-result-open">View result</button></div></div>' +
|
|
21
|
+
'<button class="notif-banner-close" aria-label="Dismiss notification">' + iconHtml("x") + '</button>';
|
|
22
|
+
container.appendChild(banner);
|
|
23
|
+
refreshIcons(banner);
|
|
24
|
+
requestAnimationFrame(function () { banner.classList.add("show"); });
|
|
25
|
+
banner.querySelector(".notif-banner-result-open").addEventListener("click", function (event) {
|
|
26
|
+
event.stopPropagation(); removeBanner(banner); if (typeof onOpen === "function") onOpen();
|
|
27
|
+
});
|
|
28
|
+
banner.querySelector(".notif-banner-close").addEventListener("click", function (event) { event.stopPropagation(); removeBanner(banner); });
|
|
29
|
+
setTimeout(function () { removeBanner(banner); }, 8000);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function scheduledTaskExecutionLabel(record) {
|
|
2
|
+
if (record.activeRun) {
|
|
3
|
+
if (record.activeRun.status === "needs-input") return "Needs input";
|
|
4
|
+
if (record.activeRun.status === "waiting-worker") return "Waiting for Worker";
|
|
5
|
+
if (record.activeRun.status === "reviewing") return "Driver reviewing";
|
|
6
|
+
return "Running";
|
|
7
|
+
}
|
|
8
|
+
if (!record.execution) return "Needs setup";
|
|
9
|
+
if (record.readiness && record.readiness.ok === false) return "Unavailable";
|
|
10
|
+
return record.enabled === false ? "Paused" : "Active";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function scheduledTaskOpenRunLabel(record) {
|
|
14
|
+
return record.activeRun && record.activeRun.status === "needs-input" ? "Open blocked run" : "Open run";
|
|
15
|
+
}
|
|
@@ -12,6 +12,7 @@ import { readExecutionRuntime, renderExecutionRuntime } from './scheduled-task-e
|
|
|
12
12
|
import { openDefaultAi } from './default-ai.js';
|
|
13
13
|
import { reconcileScheduledTaskEdit } from './scheduled-task-edit-state.js';
|
|
14
14
|
import { editorFields, fillEditor, readEditor, serverEditorData, validateEditor, handleEditorScheduleChange } from './scheduled-task-form.js';
|
|
15
|
+
import { scheduledTaskExecutionLabel, scheduledTaskOpenRunLabel } from './scheduled-task-presentation.js';
|
|
15
16
|
|
|
16
17
|
export { isStrictCron, humanRecurrence } from './scheduled-task-recurrence.js';
|
|
17
18
|
|
|
@@ -56,18 +57,6 @@ function scheduleLabel(record) {
|
|
|
56
57
|
return "No schedule";
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function executionLabel(record) {
|
|
60
|
-
if (record.activeRun) {
|
|
61
|
-
if (record.activeRun.status === "needs-input") return "Needs input";
|
|
62
|
-
if (record.activeRun.status === "waiting-worker") return "Waiting for Worker";
|
|
63
|
-
if (record.activeRun.status === "reviewing") return "Driver reviewing";
|
|
64
|
-
return "Running";
|
|
65
|
-
}
|
|
66
|
-
if (!record.execution) return "Needs setup";
|
|
67
|
-
if (record.readiness && record.readiness.ok === false) return "Unavailable";
|
|
68
|
-
return record.enabled === false ? "Paused" : "Active";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
60
|
function ensurePanel() {
|
|
72
61
|
if (panel) return;
|
|
73
62
|
var host = document.getElementById("main-panels");
|
|
@@ -185,7 +174,7 @@ function buildRow(record) {
|
|
|
185
174
|
row.className = "scheduled-task-row" + (record.enabled === false ? " paused" : "");
|
|
186
175
|
row.dataset.taskId = record.id;
|
|
187
176
|
row.setAttribute("role", "listitem");
|
|
188
|
-
var stateLabel =
|
|
177
|
+
var stateLabel = scheduledTaskExecutionLabel(record);
|
|
189
178
|
row.innerHTML =
|
|
190
179
|
'<button class="scheduled-task-summary" type="button" data-action="expand" aria-expanded="' + (expandedId === record.id ? "true" : "false") + '">' +
|
|
191
180
|
'<span class="scheduled-task-state-dot" aria-hidden="true"></span>' +
|
|
@@ -212,12 +201,12 @@ function buildRow(record) {
|
|
|
212
201
|
detail.appendChild(instructions);
|
|
213
202
|
var context = document.createElement("div");
|
|
214
203
|
context.className = "scheduled-task-context";
|
|
215
|
-
var executionState =
|
|
204
|
+
var executionState = scheduledTaskExecutionLabel(record);
|
|
216
205
|
context.textContent = (record.projectTitle || store.get('currentSlug') || "Project") + " · " + executionState + (record.execution ? " · Driver and Split Worker" : " · Choose execution pair in Edit");
|
|
217
206
|
detail.appendChild(context);
|
|
218
207
|
var actions = document.createElement("div");
|
|
219
208
|
actions.className = "scheduled-task-actions";
|
|
220
|
-
actions.innerHTML = '<button type="button" data-action="edit">' + iconHtml("pencil") + 'Edit</button><button type="button" data-action="toggle">' + iconHtml(record.enabled === false ? "play" : "pause") + (record.enabled === false ? "Resume" : "Pause") + '</button><button type="button" data-action="run">' + iconHtml("zap") + 'Run now</button>' + (record.activeRun || record.latestRun ? '<button type="button" data-action="open-run">' + iconHtml("columns-2") + '
|
|
209
|
+
actions.innerHTML = '<button type="button" data-action="edit">' + iconHtml("pencil") + 'Edit</button><button type="button" data-action="toggle">' + iconHtml(record.enabled === false ? "play" : "pause") + (record.enabled === false ? "Resume" : "Pause") + '</button><button type="button" data-action="run">' + iconHtml("zap") + 'Run now</button>' + (record.activeRun || record.latestRun ? '<button type="button" data-action="open-run">' + iconHtml("columns-2") + scheduledTaskOpenRunLabel(record) + '</button>' : '') + '<button type="button" data-action="delete" class="danger">' + iconHtml("trash-2") + 'Delete</button>';
|
|
221
210
|
detail.appendChild(actions);
|
|
222
211
|
}
|
|
223
212
|
row.appendChild(detail);
|
|
@@ -42,9 +42,11 @@ function createScheduledRunState(ctx) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function complete(id, runId, result) {
|
|
45
|
-
|
|
45
|
+
var completed = null;
|
|
46
|
+
var saved = mutate(id, function (rec) {
|
|
46
47
|
if (!rec.activeRun || rec.activeRun.runId !== runId) return false;
|
|
47
48
|
var run = Object.assign({}, rec.activeRun, result || {}, { finishedAt: Date.now() });
|
|
49
|
+
completed = run;
|
|
48
50
|
rec.lastRunAt = run.startedAt || Date.now();
|
|
49
51
|
rec.lastRunResult = run.outcome || "failed";
|
|
50
52
|
rec.runs = Array.isArray(rec.runs) ? rec.runs : [];
|
|
@@ -54,6 +56,8 @@ function createScheduledRunState(ctx) {
|
|
|
54
56
|
rec.activeRun = null;
|
|
55
57
|
return true;
|
|
56
58
|
});
|
|
59
|
+
if (saved && completed && ctx.onTerminal) ctx.onTerminal(ctx.getRecord(id), completed);
|
|
60
|
+
return saved;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
function recover(rec) {
|
|
@@ -81,7 +85,11 @@ function createScheduledRunState(ctx) {
|
|
|
81
85
|
return true;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
function notifyTerminal(rec, run) {
|
|
89
|
+
if (ctx.onTerminal && rec && run) ctx.onTerminal(rec, run);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { begin: begin, complete: complete, notifyTerminal: notifyTerminal, prepareRecord: prepareRecord, recordLegacy: recordLegacy, recover: recover, update: update };
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
module.exports = { createScheduledRunState: createScheduledRunState };
|
package/lib/scheduler.js
CHANGED
|
@@ -235,12 +235,12 @@ function attachPairFactory(ctx) {
|
|
|
235
235
|
var request = args || {};
|
|
236
236
|
var driverRuntime = preflightRuntime(request.driver || {}, "claude");
|
|
237
237
|
var workerRuntime = preflightRuntime(request.worker || {}, null);
|
|
238
|
-
var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort });
|
|
238
|
+
var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort, hidden: request.hidden === true });
|
|
239
239
|
var worker = null;
|
|
240
240
|
try {
|
|
241
241
|
driver.title = request.name ? request.name + " · Driver" : "Scheduled task · Driver";
|
|
242
242
|
if (driverRuntime.explicitEffort) driver.loopSettings = { effort: driverRuntime.effort };
|
|
243
|
-
worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort });
|
|
243
|
+
worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort, hidden: request.hidden === true });
|
|
244
244
|
worker.title = request.name ? request.name + " · Split Worker" : "Scheduled task · Split Worker";
|
|
245
245
|
if (workerRuntime.explicitEffort) worker.loopSettings = { effort: workerRuntime.effort };
|
|
246
246
|
sessionProvenance.markWorker(driver, worker, sm.sessions);
|
package/lib/sessions.js
CHANGED
|
@@ -177,6 +177,7 @@ function createSessionManager(opts) {
|
|
|
177
177
|
if (session.loopInterviewHandoff) metaObj.loopInterviewHandoff = session.loopInterviewHandoff;
|
|
178
178
|
if (session.scheduledTaskDraft) metaObj.scheduledTaskDraft = session.scheduledTaskDraft;
|
|
179
179
|
if (session.scheduledTaskRun) metaObj.scheduledTaskRun = session.scheduledTaskRun;
|
|
180
|
+
if (session.hidden) metaObj.hidden = true;
|
|
180
181
|
metaObj.sessionVisibility = sessionVisibility.normalize(session.sessionVisibility);
|
|
181
182
|
if (session.sessionVisibilityExplicit) metaObj.sessionVisibilityExplicit = true;
|
|
182
183
|
if (session.bookmarked) metaObj.bookmarked = true;
|
|
@@ -338,6 +339,7 @@ function createSessionManager(opts) {
|
|
|
338
339
|
session.loopInterviewHandoff = m.loopInterviewHandoff && typeof m.loopInterviewHandoff === "object" ? m.loopInterviewHandoff : null;
|
|
339
340
|
session.scheduledTaskDraft = m.scheduledTaskDraft && typeof m.scheduledTaskDraft === "object" ? m.scheduledTaskDraft : null;
|
|
340
341
|
session.scheduledTaskRun = m.scheduledTaskRun && typeof m.scheduledTaskRun === "object" ? m.scheduledTaskRun : null;
|
|
342
|
+
session.hidden = m.hidden === true || !!session.scheduledTaskRun;
|
|
341
343
|
session.terminalId = null;
|
|
342
344
|
session.runtimeMode = null;
|
|
343
345
|
session.runtimeTerminalId = null;
|
|
@@ -664,6 +666,7 @@ function createSessionManager(opts) {
|
|
|
664
666
|
loopInterviewHandoff: null,
|
|
665
667
|
scheduledTaskDraft: null,
|
|
666
668
|
scheduledTaskRun: null,
|
|
669
|
+
hidden: !!(sessionOpts && sessionOpts.hidden),
|
|
667
670
|
terminalId: null,
|
|
668
671
|
sessionOriginId: null,
|
|
669
672
|
};
|
|
@@ -711,6 +714,7 @@ function createSessionManager(opts) {
|
|
|
711
714
|
loopInterviewHandoff: null,
|
|
712
715
|
scheduledTaskDraft: null,
|
|
713
716
|
scheduledTaskRun: null,
|
|
717
|
+
hidden: !!(sessionOpts && sessionOpts.hidden),
|
|
714
718
|
terminalId: null,
|
|
715
719
|
sessionOriginId: null,
|
|
716
720
|
};
|
package/lib/ws-schema.js
CHANGED
|
@@ -81,6 +81,11 @@ var schema = {
|
|
|
81
81
|
"scheduled_task_update": { direction: "c2s", handler: "lib/project-scheduled-tasks.js", description: "Update an exact schedule revision" },
|
|
82
82
|
"scheduled_task_cancel": { direction: "c2s", handler: "lib/project-scheduled-tasks.js", description: "Cancel the active schedule interview or draft" },
|
|
83
83
|
"scheduled_task_action_result": { direction: "s2c", handler: "lib/public/modules/scheduled-tasks.js", description: "Correlated create or edit result" },
|
|
84
|
+
"scheduled_task_results_state": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "Authorized per-user unread Scheduled Tasks result state" },
|
|
85
|
+
"scheduled_task_result_created": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "Live terminal Scheduled Tasks result notification" },
|
|
86
|
+
"scheduled_task_result_mark_read": { direction: "c2s", handler: "lib/project-logs.js", description: "Mark an exact displayed scheduled result read for the current user" },
|
|
87
|
+
"scheduled_task_result_open_session": { direction: "c2s", handler: "lib/project-logs.js", description: "Resolve the hidden Driver for an authorized scheduled result" },
|
|
88
|
+
"scheduled_task_result_session": { direction: "s2c", handler: "lib/public/modules/project-logs.js", description: "Authorized hidden scheduled Driver navigation target" },
|
|
84
89
|
"search_results": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Session title search results" },
|
|
85
90
|
"search_content_results": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full-text content search results" },
|
|
86
91
|
"fork_complete": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Fork succeeded, includes new session ID" },
|
package/package.json
CHANGED