clay-server 3.8.1 → 4.0.0-beta.10
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/ask-user-mcp-server.js +5 -119
- package/lib/background-task-timing.js +105 -0
- package/lib/builtin-mates.js +6 -6
- package/lib/claude-hook-installer.js +15 -0
- package/lib/clay-history-mcp-server.js +62 -297
- package/lib/daemon.js +23 -1
- package/lib/debate-model-selection.js +119 -0
- package/lib/home-debate-tool-policy.js +164 -0
- package/lib/home-response-notifications.js +26 -0
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +224 -0
- package/lib/mate-creation-mcp-server.js +55 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/mate-ready-creation.js +95 -0
- package/lib/mates.js +2 -0
- package/lib/notes.js +2 -1
- package/lib/project-ask-user.js +64 -0
- package/lib/project-capsule-catalog.js +101 -0
- package/lib/project-connection.js +17 -0
- package/lib/project-debate-proposal.js +133 -17
- package/lib/project-debate.js +214 -110
- package/lib/project-delegated-follow-up.js +84 -0
- package/lib/project-delegated-session.js +53 -0
- package/lib/project-filesystem.js +7 -9
- package/lib/project-knowledge.js +23 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-mcp-server.js +290 -0
- package/lib/project-logs-query.js +167 -0
- package/lib/project-logs-root.js +67 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +357 -0
- package/lib/project-logs-snapshot.js +202 -0
- package/lib/project-logs-store.js +498 -0
- package/lib/project-logs-versioning.js +128 -0
- package/lib/project-logs.js +347 -0
- package/lib/project-mate-creation-proposal.js +120 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +36 -7
- package/lib/project-models.js +157 -32
- package/lib/project-session-notes.js +2 -2
- package/lib/project-session-pair.js +8 -8
- package/lib/project-sessions.js +66 -51
- package/lib/project-vendor-login.js +414 -0
- package/lib/project-worker-proposal.js +25 -25
- package/lib/project-workspace-query.js +144 -0
- package/lib/project.js +433 -102
- package/lib/public/app.js +109 -48
- package/lib/public/css/avatar-imprints.css +0 -1
- package/lib/public/css/base.css +4 -0
- package/lib/public/css/capsule-ui.css +389 -0
- package/lib/public/css/command-palette.css +306 -6
- package/lib/public/css/home-capsule-library.css +243 -0
- package/lib/public/css/home-capsule-source.css +192 -0
- package/lib/public/css/home-debate-live.css +146 -0
- package/lib/public/css/home-debate-planning.css +125 -0
- package/lib/public/css/home-debates-archive.css +281 -0
- package/lib/public/css/home-hub.css +896 -405
- package/lib/public/css/home-mate-creation.css +34 -0
- package/lib/public/css/home-mate-model-picker.css +131 -0
- package/lib/public/css/home-mate-settings.css +344 -0
- package/lib/public/css/home-session-actions.css +265 -0
- package/lib/public/css/home-sidebar.css +667 -0
- package/lib/public/css/icon-strip.css +1 -2
- package/lib/public/css/input.css +79 -25
- package/lib/public/css/mates.css +7 -34
- package/lib/public/css/pane.css +1 -1
- package/lib/public/css/playbook.css +0 -80
- package/lib/public/css/project-logs.css +234 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/sticky-notes.css +53 -117
- package/lib/public/css/tui-attention.css +0 -44
- package/lib/public/css/workspace-assignment.css +45 -0
- package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
- package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
- package/lib/public/index.html +96 -74
- package/lib/public/modules/app-connection.js +100 -5
- package/lib/public/modules/app-dm.js +43 -160
- package/lib/public/modules/app-home-hub.js +328 -585
- package/lib/public/modules/app-message-cards.js +242 -0
- package/lib/public/modules/app-message-router.js +84 -0
- package/lib/public/modules/app-messages.js +124 -38
- package/lib/public/modules/app-notifications.js +8 -119
- package/lib/public/modules/app-panels.js +43 -2
- package/lib/public/modules/app-projects.js +9 -1
- package/lib/public/modules/app-rendering.js +21 -602
- package/lib/public/modules/avatar-imprint.js +11 -8
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/chat-bubble-renderer.js +152 -0
- package/lib/public/modules/chat-render-runtime.js +198 -0
- package/lib/public/modules/command-palette.js +225 -436
- package/lib/public/modules/dm-render.js +82 -0
- package/lib/public/modules/home-capsule-creation-intent.js +31 -0
- package/lib/public/modules/home-capsule-library.js +146 -0
- package/lib/public/modules/home-capsule-source.js +383 -0
- package/lib/public/modules/home-chat-identity.js +35 -0
- package/lib/public/modules/home-chat-scroll.js +77 -0
- package/lib/public/modules/home-chat-stream-state.js +34 -0
- package/lib/public/modules/home-composer-focus.js +25 -0
- package/lib/public/modules/home-conversations-sheet.js +179 -0
- package/lib/public/modules/home-debate-activity.js +48 -0
- package/lib/public/modules/home-debate-controls.js +295 -0
- package/lib/public/modules/home-debate-launch.js +41 -0
- package/lib/public/modules/home-debate-live.js +284 -0
- package/lib/public/modules/home-debate-models.js +73 -0
- package/lib/public/modules/home-debate-planning.js +335 -0
- package/lib/public/modules/home-debates-archive.js +279 -0
- package/lib/public/modules/home-dock-resize.js +74 -0
- package/lib/public/modules/home-dock.js +446 -0
- package/lib/public/modules/home-mate-chat.js +496 -0
- package/lib/public/modules/home-mate-creation.js +109 -0
- package/lib/public/modules/home-mate-model-picker.js +281 -0
- package/lib/public/modules/home-mate-selection.js +38 -0
- package/lib/public/modules/home-mate-settings-menu.js +131 -0
- package/lib/public/modules/home-mate-settings.js +383 -0
- package/lib/public/modules/home-session-actions.js +373 -0
- package/lib/public/modules/home-session-model-confirmation.js +13 -0
- package/lib/public/modules/home-shell.js +9 -0
- package/lib/public/modules/home-sidebar-chat-list.js +63 -0
- package/lib/public/modules/home-sidebar.js +165 -0
- package/lib/public/modules/home-sub-surface.js +19 -0
- package/lib/public/modules/home-surface-boot.js +68 -0
- package/lib/public/modules/home-surface.js +141 -0
- package/lib/public/modules/home-tools.js +271 -0
- package/lib/public/modules/markdown.js +50 -2
- package/lib/public/modules/mate-management.js +86 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/mate-wizard.js +0 -7
- package/lib/public/modules/mention.js +17 -19
- package/lib/public/modules/pane-bridge.js +9 -0
- package/lib/public/modules/profile.js +4 -5
- package/lib/public/modules/project-logs-ambient.js +238 -0
- package/lib/public/modules/project-logs-render.js +359 -0
- package/lib/public/modules/project-logs.js +328 -0
- package/lib/public/modules/project-settings.js +34 -28
- package/lib/public/modules/project-switcher.js +11 -5
- package/lib/public/modules/project-workspace-assignment.js +23 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/search-clay-chat.js +402 -0
- package/lib/public/modules/server-settings.js +14 -12
- package/lib/public/modules/session-actions.js +1 -1
- package/lib/public/modules/sidebar-mates.js +56 -552
- package/lib/public/modules/sidebar-mobile.js +12 -73
- package/lib/public/modules/sidebar-presence.js +37 -0
- package/lib/public/modules/sidebar-sessions.js +25 -1
- package/lib/public/modules/split-pair-ui.js +13 -13
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/sticky-notes.js +106 -53
- package/lib/public/modules/tool-input-composition.js +48 -0
- package/lib/public/modules/tool-llm-status.js +167 -0
- package/lib/public/modules/tool-palette.js +76 -4
- package/lib/public/modules/tool-renderer-advanced.js +285 -0
- package/lib/public/modules/tool-renderer-chart.js +158 -0
- package/lib/public/modules/tool-renderer-semantics.js +35 -0
- package/lib/public/modules/tool-renderer.js +464 -0
- package/lib/public/modules/tool-runtime.js +309 -0
- package/lib/public/modules/tool-ui-evaluator.js +97 -0
- package/lib/public/modules/tool-ui-tree.js +21 -0
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/user-settings.js +16 -23
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/public/modules/worker-proposal.js +2 -2
- package/lib/public/modules/workspace-assignment-card.js +149 -0
- package/lib/public/modules/workspace-assignment-routing.js +4 -0
- package/lib/public/style.css +14 -2
- package/lib/runtime-env.js +65 -0
- package/lib/sdk-bridge.js +120 -32
- package/lib/sdk-message-processor.js +34 -13
- package/lib/server-home-capsule-creation.js +36 -0
- package/lib/server-home-chat-events.js +287 -0
- package/lib/server-home-chat.js +518 -0
- package/lib/server-home-clay-entry.js +65 -0
- package/lib/server-home-clay-session-links.js +26 -0
- package/lib/server-home-debate-planning.js +214 -0
- package/lib/server-home-debates.js +137 -0
- package/lib/server-home-mate-creation.js +261 -0
- package/lib/server-home-models.js +241 -0
- package/lib/server-home-preferences.js +61 -0
- package/lib/server-mates.js +37 -0
- package/lib/server-tools.js +377 -0
- package/lib/server.js +166 -59
- package/lib/session-pair-mcp-server.js +6 -6
- package/lib/sessions.js +22 -4
- package/lib/tool-capsule-source.js +142 -0
- package/lib/tool-control-mcp-server.js +126 -0
- package/lib/tool-llm.js +48 -0
- package/lib/tool-storage.js +77 -0
- package/lib/tool-ui-spec-advanced.js +82 -0
- package/lib/tool-ui-spec.js +261 -0
- package/lib/tools-registry.js +286 -0
- package/lib/users-home-dock-preferences.js +77 -0
- package/lib/users-home-surface-preferences.js +99 -0
- package/lib/users-preferences.js +4 -7
- package/lib/users.js +12 -0
- package/lib/whats-new-content.js +3 -3
- package/lib/workspace-assignment-service.js +420 -0
- package/lib/workspace-query-mcp-server.js +154 -0
- package/lib/workspace-query-service.js +492 -0
- package/lib/ws-schema.js +107 -12
- package/lib/yoke/adapters/acp.js +2 -1
- package/lib/yoke/adapters/antigravity.js +2 -1
- package/lib/yoke/adapters/claude-worker.js +11 -4
- package/lib/yoke/adapters/claude.js +141 -46
- package/lib/yoke/adapters/codex.js +79 -11
- package/lib/yoke/adapters/kiro.js +2 -1
- package/lib/yoke/claude-user-input.js +38 -0
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/codex-user-input.js +64 -0
- package/lib/yoke/complete-once.js +164 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/interface.js +19 -0
- package/lib/yoke/mcp-bridge-server.js +2 -6
- package/lib/yoke/user-input.js +254 -0
- package/package.json +4 -3
- package/lib/public/css/home-chat.css +0 -380
- package/lib/public/modules/home-chat.js +0 -438
- package/lib/server-clay-home.js +0 -245
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
// Compatibility write-through bridge for Mate durable memory and knowledge.
|
|
2
|
+
//
|
|
3
|
+
// The startup importer alone goes stale the moment a Mate writes a digest or a
|
|
4
|
+
// user edits a knowledge file. This module keeps the Knowledge scope current
|
|
5
|
+
// after every known mutation, without changing how the legacy files are
|
|
6
|
+
// written. Legacy files stay authoritative and rollback-compatible; the new
|
|
7
|
+
// backend is a continuously reconciled mirror.
|
|
8
|
+
//
|
|
9
|
+
// Reconciliation is source-level, not event-level. A caller says "this source
|
|
10
|
+
// changed" and this module re-reads that one file and makes the scope match it:
|
|
11
|
+
// creating, revising, reviving, or tombstoning as required. That makes every
|
|
12
|
+
// call idempotent and makes deletions and rewrites work without the call site
|
|
13
|
+
// having to describe what changed.
|
|
14
|
+
//
|
|
15
|
+
// Identity is never mirrored. CLAUDE.md, base-template.md, identity-backup.md,
|
|
16
|
+
// and identity-history.jsonl define who a Mate is and remain canonical files.
|
|
17
|
+
// Sticky notes are a separate attention layer and are excluded too.
|
|
18
|
+
|
|
19
|
+
var fs = require("fs");
|
|
20
|
+
var path = require("path");
|
|
21
|
+
var crypto = require("crypto");
|
|
22
|
+
var config = require("./config");
|
|
23
|
+
var knowledgeImport = require("./knowledge-import");
|
|
24
|
+
|
|
25
|
+
var SOURCE_NAMESPACE = "mate-knowledge";
|
|
26
|
+
var SOURCE_VERSION = 2;
|
|
27
|
+
var MAX_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
// Identity and template artifacts. Never mirrored, at any version.
|
|
30
|
+
var IDENTITY_FILES = ["base-template.md", "identity-backup.md", "identity-history.jsonl"];
|
|
31
|
+
// Sticky notes remain the separate attention layer, with their own store.
|
|
32
|
+
var EXCLUDED_FILES = ["sticky-notes.md"];
|
|
33
|
+
|
|
34
|
+
// Line-addressed journals get one record per line, keyed by line content.
|
|
35
|
+
var JOURNAL_KINDS = {
|
|
36
|
+
"session-digests.jsonl": "session-digest",
|
|
37
|
+
"user-observations.jsonl": "user-observation",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function matesHome() {
|
|
41
|
+
return path.resolve(path.join(config.CONFIG_DIR, "mates"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Derived from the Mate storage root path alone, so the startup importer and
|
|
45
|
+
// the live bridge always agree on a scope without needing the same context.
|
|
46
|
+
function ownerKeyForRoot(matesRoot) {
|
|
47
|
+
var resolved = path.resolve(matesRoot);
|
|
48
|
+
var parent = path.dirname(resolved);
|
|
49
|
+
var name = path.basename(resolved);
|
|
50
|
+
if (parent === matesHome() && name.indexOf("mate_") !== 0) return "u-" + name;
|
|
51
|
+
return "h-" + crypto.createHash("sha256").update(resolved).digest("hex").substring(0, 16);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scopeIdFor(ownerKey, mateId) {
|
|
55
|
+
return "mate/" + ownerKey + "/" + mateId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A Mate project's cwd is its Mate directory, so every call site can pass what
|
|
59
|
+
// it already has.
|
|
60
|
+
function scopeForMateDir(mateDir) {
|
|
61
|
+
var resolved = path.resolve(mateDir);
|
|
62
|
+
var mateId = path.basename(resolved);
|
|
63
|
+
if (mateId.indexOf("mate_") !== 0) return null;
|
|
64
|
+
var matesRoot = path.dirname(resolved);
|
|
65
|
+
var ownerKey = ownerKeyForRoot(matesRoot);
|
|
66
|
+
var parent = path.dirname(path.resolve(matesRoot));
|
|
67
|
+
return {
|
|
68
|
+
mateId: mateId,
|
|
69
|
+
mateDir: resolved,
|
|
70
|
+
matesRoot: matesRoot,
|
|
71
|
+
ownerKey: ownerKey,
|
|
72
|
+
userId: parent === matesHome() ? path.basename(matesRoot) : null,
|
|
73
|
+
scopeId: scopeIdFor(ownerKey, mateId),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sourceKindFor(name) {
|
|
78
|
+
if (IDENTITY_FILES.indexOf(name) !== -1) return null;
|
|
79
|
+
if (EXCLUDED_FILES.indexOf(name) !== -1) return null;
|
|
80
|
+
if (name === "memory-summary.md") return "memory-summary";
|
|
81
|
+
if (JOURNAL_KINDS[name]) return JOURNAL_KINDS[name];
|
|
82
|
+
if (/\.md$/.test(name)) return "knowledge-file";
|
|
83
|
+
if (/\.jsonl$/.test(name)) return "knowledge-journal";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isJournal(name) {
|
|
88
|
+
return !!JOURNAL_KINDS[name] || /\.jsonl$/.test(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeActor(actor) {
|
|
92
|
+
var source = actor || {};
|
|
93
|
+
var type = ["user", "agent", "system", "migration"].indexOf(source.type) !== -1 ? source.type : "system";
|
|
94
|
+
return {
|
|
95
|
+
type: type,
|
|
96
|
+
userId: source.userId ? String(source.userId).substring(0, 120) : null,
|
|
97
|
+
displayName: source.displayName ? String(source.displayName).substring(0, 120) : null,
|
|
98
|
+
sessionKey: source.sessionKey ? String(source.sessionKey).substring(0, 200) : null,
|
|
99
|
+
vendor: source.vendor ? String(source.vendor).substring(0, 40) : null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Relative to the Mate directory. The scope already identifies the owner and
|
|
104
|
+
// the Mate, so an absolute filesystem path would add nothing but exposure.
|
|
105
|
+
function relativeSource(name) {
|
|
106
|
+
return "knowledge/" + name;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function provenance(scope, name, extra) {
|
|
110
|
+
var base = {
|
|
111
|
+
namespace: SOURCE_NAMESPACE,
|
|
112
|
+
sourceVersion: SOURCE_VERSION,
|
|
113
|
+
relPath: relativeSource(name),
|
|
114
|
+
fileName: name,
|
|
115
|
+
ownerKey: scope.ownerKey,
|
|
116
|
+
userId: scope.userId,
|
|
117
|
+
mateId: scope.mateId,
|
|
118
|
+
mateName: scope.mateName || null,
|
|
119
|
+
};
|
|
120
|
+
if (!extra) return base;
|
|
121
|
+
return Object.assign(base, extra);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Identify a live record that this module wrote, by provenance rather than by
|
|
125
|
+
// parsing an import key. A record from another namespace, or from a source
|
|
126
|
+
// version this build does not understand, is left strictly alone: reconciling
|
|
127
|
+
// it could tombstone something whose meaning we do not know.
|
|
128
|
+
function isMateSourceEntry(entry) {
|
|
129
|
+
if (!entry || entry.deleted === true) return false;
|
|
130
|
+
if (entry.kind === "chunk") return false;
|
|
131
|
+
var source = entry.source;
|
|
132
|
+
if (!source || source.namespace !== SOURCE_NAMESPACE) return false;
|
|
133
|
+
if (typeof source.sourceVersion !== "number" || source.sourceVersion > SOURCE_VERSION) return false;
|
|
134
|
+
var name = source.fileName;
|
|
135
|
+
if (!name || typeof name !== "string") return false;
|
|
136
|
+
// The file name and the relative path must agree, so neither field alone is
|
|
137
|
+
// trusted, and the name must be a plain basename.
|
|
138
|
+
if (path.basename(name) !== name) return false;
|
|
139
|
+
return source.relPath === relativeSource(name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fileKey(name) {
|
|
143
|
+
return "file:" + name;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function journalKey(name, hash) {
|
|
147
|
+
return "journal:" + name + ":" + hash.substring(0, 24);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function newSummary() {
|
|
151
|
+
return {
|
|
152
|
+
created: 0,
|
|
153
|
+
revised: 0,
|
|
154
|
+
revived: 0,
|
|
155
|
+
unchanged: 0,
|
|
156
|
+
deleted: 0,
|
|
157
|
+
absent: 0,
|
|
158
|
+
"already-deleted": 0,
|
|
159
|
+
chunked: 0,
|
|
160
|
+
malformedLines: 0,
|
|
161
|
+
identitySkipped: 0,
|
|
162
|
+
excluded: 0,
|
|
163
|
+
unsupported: 0,
|
|
164
|
+
empty: 0,
|
|
165
|
+
failed: 0,
|
|
166
|
+
errors: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function record(summary, action) {
|
|
171
|
+
if (summary[action] === undefined) summary[action] = 0;
|
|
172
|
+
summary[action]++;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readStat(filePath) {
|
|
176
|
+
try {
|
|
177
|
+
return fs.statSync(filePath);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- File-addressed sources -------------------------------------------
|
|
184
|
+
|
|
185
|
+
function syncFile(importer, scope, name, kind, filePath, stat, actor, summary) {
|
|
186
|
+
if (!stat) {
|
|
187
|
+
// The legacy file is gone. Tombstone rather than leaving stale content live.
|
|
188
|
+
var removed = importer.removeRecord(fileKey(name), {
|
|
189
|
+
actor: actor,
|
|
190
|
+
source: provenance(scope, name, { type: kind, reason: "source-removed" }),
|
|
191
|
+
});
|
|
192
|
+
record(summary, removed.action);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (stat.size > MAX_SOURCE_BYTES) {
|
|
196
|
+
summary.failed++;
|
|
197
|
+
summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Source exceeds the maximum representable size." });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
var content;
|
|
201
|
+
try {
|
|
202
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
203
|
+
} catch (e) {
|
|
204
|
+
summary.failed++;
|
|
205
|
+
summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Unreadable source: " + e.message });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!content.trim()) {
|
|
209
|
+
// An emptied file is a logical removal, not a blank record.
|
|
210
|
+
var emptied = importer.removeRecord(fileKey(name), {
|
|
211
|
+
actor: actor,
|
|
212
|
+
source: provenance(scope, name, { type: kind, reason: "source-emptied" }),
|
|
213
|
+
});
|
|
214
|
+
summary.empty++;
|
|
215
|
+
if (emptied.action === "deleted") summary.deleted++;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
var result = importer.importRecord({
|
|
219
|
+
importKey: fileKey(name),
|
|
220
|
+
sourceHash: knowledgeImport.contentHash(content),
|
|
221
|
+
kind: kind,
|
|
222
|
+
name: name,
|
|
223
|
+
content: content,
|
|
224
|
+
at: Math.round(stat.mtimeMs),
|
|
225
|
+
actor: actor,
|
|
226
|
+
source: provenance(scope, name, {
|
|
227
|
+
type: kind,
|
|
228
|
+
sourceBytes: stat.size,
|
|
229
|
+
sourceModifiedAt: stat.mtimeMs,
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
record(summary, result.action);
|
|
233
|
+
if (content.length > knowledgeImport.MAX_CONTENT_CHARS) summary.chunked++;
|
|
234
|
+
// Verified whenever something was actually written. An unchanged source was
|
|
235
|
+
// already verified when it landed, so the steady-state path stays cheap.
|
|
236
|
+
if (result.action !== "unchanged") verifyRoundTrip(importer, fileKey(name), scope, name, summary);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- Line-addressed journals ------------------------------------------
|
|
240
|
+
|
|
241
|
+
// Reconciles the whole journal: every present line is imported, and every line
|
|
242
|
+
// previously imported from this journal that is no longer present is
|
|
243
|
+
// tombstoned. A digest deleted through the memory UI therefore stops being a
|
|
244
|
+
// live memory instead of lingering in the new backend.
|
|
245
|
+
function syncJournal(importer, scope, name, kind, filePath, stat, actor, summary) {
|
|
246
|
+
var raw = "";
|
|
247
|
+
if (stat) {
|
|
248
|
+
if (stat.size > MAX_SOURCE_BYTES) {
|
|
249
|
+
summary.failed++;
|
|
250
|
+
summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Source exceeds the maximum representable size." });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
255
|
+
} catch (e) {
|
|
256
|
+
summary.failed++;
|
|
257
|
+
summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Unreadable source: " + e.message });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var present = {};
|
|
263
|
+
var lines = raw.split("\n");
|
|
264
|
+
for (var i = 0; i < lines.length; i++) {
|
|
265
|
+
var line = lines[i].trim();
|
|
266
|
+
if (!line) continue;
|
|
267
|
+
var parsed = null;
|
|
268
|
+
try {
|
|
269
|
+
parsed = JSON.parse(line);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
summary.malformedLines++;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
// A journal entry is a plain object. An array or a bare scalar parses
|
|
275
|
+
// cleanly but is not a record, so it is skipped rather than imported.
|
|
276
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
277
|
+
summary.malformedLines++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
var hash = knowledgeImport.contentHash(line);
|
|
281
|
+
var key = journalKey(name, hash);
|
|
282
|
+
present[key] = true;
|
|
283
|
+
var result = importer.importRecord({
|
|
284
|
+
importKey: key,
|
|
285
|
+
sourceHash: hash,
|
|
286
|
+
kind: kind,
|
|
287
|
+
name: name,
|
|
288
|
+
content: line,
|
|
289
|
+
actor: actor,
|
|
290
|
+
source: provenance(scope, name, {
|
|
291
|
+
type: kind,
|
|
292
|
+
lineIndex: i,
|
|
293
|
+
sourceDate: typeof parsed.date === "string" ? parsed.date : null,
|
|
294
|
+
sourceBytes: Buffer.byteLength(line, "utf8"),
|
|
295
|
+
sourceModifiedAt: stat ? stat.mtimeMs : null,
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
record(summary, result.action);
|
|
299
|
+
if (line.length > knowledgeImport.MAX_CONTENT_CHARS) summary.chunked++;
|
|
300
|
+
if (result.action === "created" || result.action === "revived") {
|
|
301
|
+
verifyRoundTrip(importer, key, scope, name, summary);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Every live line this module imported from this journal that the legacy
|
|
306
|
+
// file no longer contains. Selected by provenance, so a record from another
|
|
307
|
+
// namespace sharing a file name is never touched.
|
|
308
|
+
var stale = [];
|
|
309
|
+
var live = importer.entries();
|
|
310
|
+
for (var e = 0; e < live.length; e++) {
|
|
311
|
+
if (!isMateSourceEntry(live[e])) continue;
|
|
312
|
+
if (live[e].source.fileName !== name) continue;
|
|
313
|
+
if (present[live[e].importKey]) continue;
|
|
314
|
+
stale.push(live[e].importKey);
|
|
315
|
+
}
|
|
316
|
+
for (var s = 0; s < stale.length; s++) {
|
|
317
|
+
var removed = importer.removeRecord(stale[s], {
|
|
318
|
+
actor: actor,
|
|
319
|
+
source: provenance(scope, name, { type: kind, reason: "line-removed" }),
|
|
320
|
+
});
|
|
321
|
+
record(summary, removed.action);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// No source may be reported as successfully mirrored while its content cannot
|
|
326
|
+
// be read back exactly.
|
|
327
|
+
function verifyRoundTrip(importer, importKey, scope, name, summary) {
|
|
328
|
+
var entries = importer.entries();
|
|
329
|
+
var entry = null;
|
|
330
|
+
for (var i = 0; i < entries.length; i++) {
|
|
331
|
+
if (entries[i].importKey === importKey) { entry = entries[i]; break; }
|
|
332
|
+
}
|
|
333
|
+
if (!entry) {
|
|
334
|
+
summary.failed++;
|
|
335
|
+
summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Imported record is not readable: " + importKey });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
var read = importer.readContent(entry);
|
|
339
|
+
if (!read.complete) {
|
|
340
|
+
summary.failed++;
|
|
341
|
+
summary.errors.push({
|
|
342
|
+
scopeId: scope.scopeId,
|
|
343
|
+
file: name,
|
|
344
|
+
message: (entry.chunked ? "Chunked source did not reassemble: " : "Stored source did not verify: ") + (read.reason || "unknown"),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- Public API --------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
function importerFor(scope, baseDir) {
|
|
352
|
+
return knowledgeImport.createKnowledgeImporter({ scopeId: scope.scopeId, baseDir: baseDir });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Reconcile exactly one Mate knowledge source against the new backend. Safe to
|
|
356
|
+
// call after any legacy write, including a delete. Never throws.
|
|
357
|
+
function syncMateSource(opts) {
|
|
358
|
+
var options = opts || {};
|
|
359
|
+
var summary = options.summary || newSummary();
|
|
360
|
+
try {
|
|
361
|
+
var scope = options.scope || scopeForMateDir(options.mateDir);
|
|
362
|
+
if (!scope) return summary;
|
|
363
|
+
if (options.mateName) scope.mateName = options.mateName;
|
|
364
|
+
var name = path.basename(String(options.fileName || ""));
|
|
365
|
+
if (!name) return summary;
|
|
366
|
+
if (IDENTITY_FILES.indexOf(name) !== -1) { summary.identitySkipped++; return summary; }
|
|
367
|
+
if (EXCLUDED_FILES.indexOf(name) !== -1) { summary.excluded++; return summary; }
|
|
368
|
+
var kind = sourceKindFor(name);
|
|
369
|
+
if (!kind) { summary.unsupported++; return summary; }
|
|
370
|
+
|
|
371
|
+
var actor = normalizeActor(options.actor);
|
|
372
|
+
var importer = options.importer || importerFor(scope, options.baseDir);
|
|
373
|
+
var filePath = path.join(scope.mateDir, "knowledge", name);
|
|
374
|
+
var stat = readStat(filePath);
|
|
375
|
+
|
|
376
|
+
if (isJournal(name)) syncJournal(importer, scope, name, kind, filePath, stat, actor, summary);
|
|
377
|
+
else syncFile(importer, scope, name, kind, filePath, stat, actor, summary);
|
|
378
|
+
} catch (e) {
|
|
379
|
+
summary.failed++;
|
|
380
|
+
summary.errors.push({ message: e.message });
|
|
381
|
+
}
|
|
382
|
+
return summary;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Alias that reads better at a call site reacting to a removal or a rewrite.
|
|
386
|
+
function reconcileSource(opts) {
|
|
387
|
+
return syncMateSource(opts);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Reconcile every durable source in one Mate directory, in both directions.
|
|
391
|
+
//
|
|
392
|
+
// Enumerating the directory alone is not enough. A knowledge file or a whole
|
|
393
|
+
// journal can be deleted while the daemon is stopped, or by direct filesystem
|
|
394
|
+
// tooling, and nothing would ever call syncMateSource for it again. So the
|
|
395
|
+
// scope's own live records are also consulted: any source this module
|
|
396
|
+
// previously imported that is no longer on disk is reconciled, which tombstones
|
|
397
|
+
// a missing file record or every line of a missing journal.
|
|
398
|
+
function reconcileMate(opts) {
|
|
399
|
+
var options = opts || {};
|
|
400
|
+
var summary = options.summary || newSummary();
|
|
401
|
+
var scope = options.scope || scopeForMateDir(options.mateDir);
|
|
402
|
+
if (!scope) return summary;
|
|
403
|
+
if (options.mateName) scope.mateName = options.mateName;
|
|
404
|
+
var knowledgeDir = path.join(scope.mateDir, "knowledge");
|
|
405
|
+
var present = {};
|
|
406
|
+
var entries = [];
|
|
407
|
+
try {
|
|
408
|
+
entries = fs.readdirSync(knowledgeDir, { withFileTypes: true });
|
|
409
|
+
} catch (e) {
|
|
410
|
+
entries = [];
|
|
411
|
+
}
|
|
412
|
+
var importer = options.importer || importerFor(scope, options.baseDir);
|
|
413
|
+
|
|
414
|
+
function reconcileOne(fileName) {
|
|
415
|
+
syncMateSource({
|
|
416
|
+
scope: scope,
|
|
417
|
+
fileName: fileName,
|
|
418
|
+
actor: options.actor,
|
|
419
|
+
importer: importer,
|
|
420
|
+
baseDir: options.baseDir,
|
|
421
|
+
summary: summary,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (var i = 0; i < entries.length; i++) {
|
|
426
|
+
if (!entries[i].isFile()) continue;
|
|
427
|
+
// Recorded as present whatever its kind, so an excluded or unsupported file
|
|
428
|
+
// that still exists is never mistaken for a deleted source.
|
|
429
|
+
present[entries[i].name] = true;
|
|
430
|
+
reconcileOne(entries[i].name);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
var absent = {};
|
|
434
|
+
var live = importer.entries();
|
|
435
|
+
for (var e = 0; e < live.length; e++) {
|
|
436
|
+
if (!isMateSourceEntry(live[e])) continue;
|
|
437
|
+
var name = live[e].source.fileName;
|
|
438
|
+
if (present[name] || absent[name]) continue;
|
|
439
|
+
absent[name] = true;
|
|
440
|
+
}
|
|
441
|
+
var absentNames = Object.keys(absent);
|
|
442
|
+
for (var a = 0; a < absentNames.length; a++) reconcileOne(absentNames[a]);
|
|
443
|
+
return summary;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Fire-and-forget wrapper for legacy write paths. A mirroring failure must
|
|
447
|
+
// never break the legacy write that already succeeded.
|
|
448
|
+
function syncQuietly(opts) {
|
|
449
|
+
try {
|
|
450
|
+
var summary = syncMateSource(opts);
|
|
451
|
+
if (summary.failed > 0 && summary.errors.length > 0) {
|
|
452
|
+
console.error("[knowledge-sync] Mate knowledge mirror failed:", summary.errors[0].message);
|
|
453
|
+
}
|
|
454
|
+
return summary;
|
|
455
|
+
} catch (e) {
|
|
456
|
+
console.error("[knowledge-sync] Mate knowledge mirror failed:", e.message);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = {
|
|
462
|
+
SOURCE_NAMESPACE: SOURCE_NAMESPACE,
|
|
463
|
+
SOURCE_VERSION: SOURCE_VERSION,
|
|
464
|
+
MAX_SOURCE_BYTES: MAX_SOURCE_BYTES,
|
|
465
|
+
IDENTITY_FILES: IDENTITY_FILES,
|
|
466
|
+
EXCLUDED_FILES: EXCLUDED_FILES,
|
|
467
|
+
JOURNAL_KINDS: JOURNAL_KINDS,
|
|
468
|
+
ownerKeyForRoot: ownerKeyForRoot,
|
|
469
|
+
scopeIdFor: scopeIdFor,
|
|
470
|
+
scopeForMateDir: scopeForMateDir,
|
|
471
|
+
sourceKindFor: sourceKindFor,
|
|
472
|
+
isMateSourceEntry: isMateSourceEntry,
|
|
473
|
+
normalizeActor: normalizeActor,
|
|
474
|
+
newSummary: newSummary,
|
|
475
|
+
fileKey: fileKey,
|
|
476
|
+
journalKey: journalKey,
|
|
477
|
+
importerFor: importerFor,
|
|
478
|
+
syncMateSource: syncMateSource,
|
|
479
|
+
reconcileSource: reconcileSource,
|
|
480
|
+
reconcileMate: reconcileMate,
|
|
481
|
+
syncQuietly: syncQuietly,
|
|
482
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Atomic finalization of an interviewed Mate. No placeholder is exposed unless
|
|
2
|
+
// the complete identity has passed validation and is ready to register.
|
|
3
|
+
|
|
4
|
+
var fs = require("fs");
|
|
5
|
+
var path = require("path");
|
|
6
|
+
|
|
7
|
+
function cleanLine(value, maxLength, field) {
|
|
8
|
+
if (typeof value !== "string") throw new Error(field + " is required.");
|
|
9
|
+
var clean = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
10
|
+
if (!clean) throw new Error(field + " is required.");
|
|
11
|
+
return clean.slice(0, maxLength);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cleanList(value, maxItems) {
|
|
15
|
+
var source = Array.isArray(value) ? value : [];
|
|
16
|
+
var result = [];
|
|
17
|
+
for (var i = 0; i < source.length && result.length < maxItems; i++) {
|
|
18
|
+
if (typeof source[i] !== "string") continue;
|
|
19
|
+
var clean = source[i].replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
20
|
+
if (clean) result.push(clean);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalize(definition) {
|
|
26
|
+
var identity = typeof definition.identityMarkdown === "string" ? definition.identityMarkdown.replace(/\u0000/g, "").trim() : "";
|
|
27
|
+
if (identity.length < 200) throw new Error("The Mate identity is incomplete.");
|
|
28
|
+
if (identity.length > 30000) throw new Error("The Mate identity is too long.");
|
|
29
|
+
return {
|
|
30
|
+
name: cleanLine(definition.name, 80, "Mate name"),
|
|
31
|
+
bio: cleanLine(definition.bio, 280, "Mate bio"),
|
|
32
|
+
relationship: cleanLine(definition.relationship || "collaborator", 120, "Working relationship"),
|
|
33
|
+
activities: cleanList(definition.activities, 12),
|
|
34
|
+
communicationStyle: cleanList(definition.communicationStyle, 12),
|
|
35
|
+
autonomy: cleanLine(definition.autonomy || "Ask before consequential actions.", 500, "Autonomy"),
|
|
36
|
+
identityMarkdown: identity,
|
|
37
|
+
vendor: cleanLine(definition.vendor || "claude", 40, "Vendor"),
|
|
38
|
+
model: cleanLine(definition.model, 240, "Mate model"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createReadyMate(mates, mateCtx, definition) {
|
|
43
|
+
var ready = normalize(definition || {});
|
|
44
|
+
if (typeof mates.extractIdentity === "function" && mates.extractIdentity(ready.identityMarkdown).trim() !== ready.identityMarkdown.trim()) {
|
|
45
|
+
throw new Error("The proposed identity contains reserved system sections.");
|
|
46
|
+
}
|
|
47
|
+
var mate = null;
|
|
48
|
+
try {
|
|
49
|
+
mate = mates.createMate(mateCtx, {
|
|
50
|
+
relationship: ready.relationship,
|
|
51
|
+
activity: ready.activities,
|
|
52
|
+
communicationStyle: ready.communicationStyle,
|
|
53
|
+
vendor: ready.vendor,
|
|
54
|
+
model: ready.model,
|
|
55
|
+
autonomy: ready.autonomy,
|
|
56
|
+
});
|
|
57
|
+
var mateDir = mates.getMateDir(mateCtx, mate.id);
|
|
58
|
+
var identityPath = path.join(mateDir, "CLAUDE.md");
|
|
59
|
+
fs.writeFileSync(identityPath, ready.identityMarkdown + "\n");
|
|
60
|
+
mates.enforceAllSections(identityPath, { ctx: mateCtx, mateId: mate.id });
|
|
61
|
+
fs.mkdirSync(path.join(mateDir, "knowledge"), { recursive: true });
|
|
62
|
+
var yaml = "# Mate metadata\n";
|
|
63
|
+
yaml += "id: " + JSON.stringify(mate.id) + "\n";
|
|
64
|
+
yaml += "name: " + JSON.stringify(ready.name) + "\n";
|
|
65
|
+
yaml += "status: ready\n";
|
|
66
|
+
yaml += "createdBy: " + JSON.stringify(mate.createdBy) + "\n";
|
|
67
|
+
yaml += "createdAt: " + mate.createdAt + "\n";
|
|
68
|
+
yaml += "relationship: " + JSON.stringify(ready.relationship) + "\n";
|
|
69
|
+
yaml += "activities: " + JSON.stringify(ready.activities) + "\n";
|
|
70
|
+
yaml += "communicationStyle: " + JSON.stringify(ready.communicationStyle) + "\n";
|
|
71
|
+
yaml += "autonomy: " + JSON.stringify(ready.autonomy) + "\n";
|
|
72
|
+
yaml += "vendor: " + JSON.stringify(ready.vendor) + "\n";
|
|
73
|
+
yaml += "model: " + JSON.stringify(ready.model) + "\n";
|
|
74
|
+
fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
|
|
75
|
+
var updated = mates.updateMate(mateCtx, mate.id, {
|
|
76
|
+
name: ready.name,
|
|
77
|
+
bio: ready.bio,
|
|
78
|
+
status: "ready",
|
|
79
|
+
vendor: ready.vendor,
|
|
80
|
+
model: ready.model,
|
|
81
|
+
profile: Object.assign({}, mate.profile || {}, { displayName: ready.name, bio: ready.bio }),
|
|
82
|
+
seedData: Object.assign({}, mate.seedData || {}, { relationship: ready.relationship, activity: ready.activities, communicationStyle: ready.communicationStyle, autonomy: ready.autonomy }),
|
|
83
|
+
});
|
|
84
|
+
mates.backupIdentity(mateDir, ready.identityMarkdown);
|
|
85
|
+
mates.logIdentityChange(mateDir, "clay_interview", ready.identityMarkdown, "");
|
|
86
|
+
return updated;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (mate && mate.id) {
|
|
89
|
+
try { mates.deleteMate(mateCtx, mate.id); } catch (cleanupError) {}
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { createReadyMate: createReadyMate, normalize: normalize };
|
package/lib/mates.js
CHANGED
|
@@ -128,6 +128,7 @@ function createMate(ctx, seedData) {
|
|
|
128
128
|
createdAt: Date.now(),
|
|
129
129
|
seedData: seedData || {},
|
|
130
130
|
vendor: (seedData && seedData.vendor) || "claude",
|
|
131
|
+
model: (seedData && seedData.model) || null,
|
|
131
132
|
profile: {
|
|
132
133
|
displayName: null,
|
|
133
134
|
avatarColor: "#5857fc",
|
|
@@ -156,6 +157,7 @@ function createMate(ctx, seedData) {
|
|
|
156
157
|
yaml += "relationship: " + (seedData.relationship || "assistant") + "\n";
|
|
157
158
|
yaml += "activities: " + JSON.stringify(seedData.activity || []) + "\n";
|
|
158
159
|
yaml += "vendor: " + (seedData.vendor || "claude") + "\n";
|
|
160
|
+
yaml += "model: " + JSON.stringify(seedData.model || null) + "\n";
|
|
159
161
|
fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
|
|
160
162
|
|
|
161
163
|
// Write initial CLAUDE.md (will be replaced by interview)
|
package/lib/notes.js
CHANGED
|
@@ -53,7 +53,8 @@ function createNotesManager(opts) {
|
|
|
53
53
|
y: typeof data.y === "number" ? data.y : 100,
|
|
54
54
|
w: typeof data.w === "number" ? data.w : 240,
|
|
55
55
|
h: typeof data.h === "number" ? data.h : 180,
|
|
56
|
-
color: data.color || "
|
|
56
|
+
color: data.color || "purple",
|
|
57
|
+
opacity: typeof data.opacity === "number" ? data.opacity : 0.64,
|
|
57
58
|
minimized: false,
|
|
58
59
|
zIndex: notes.length + 1,
|
|
59
60
|
createdAt: now,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
var userInput = require("./yoke/user-input");
|
|
2
|
+
|
|
3
|
+
function attachAskUser(ctx) {
|
|
4
|
+
function isFirstDebateQuestion(session) {
|
|
5
|
+
var history = session && Array.isArray(session.history) ? session.history : [];
|
|
6
|
+
for (var i = 0; i < history.length; i++) {
|
|
7
|
+
if (history[i] && history[i].type === "tool_executing" && history[i].name === "AskUserQuestion") return false;
|
|
8
|
+
}
|
|
9
|
+
return !!(session && session.homeDebatePlanning === true);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function planningQuestions(session, questions) {
|
|
13
|
+
if (!isFirstDebateQuestion(session)) return questions;
|
|
14
|
+
var first = questions[0] || {};
|
|
15
|
+
return [Object.assign({}, first, { multiSelect: false, options: [] })];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function handleRequest(boundSession, request, respond) {
|
|
19
|
+
if (!boundSession) {
|
|
20
|
+
respond.cancel("This question is not bound to an active session.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (boundSession.loop && boundSession.loop.active && boundSession.loop.role !== "crafting") {
|
|
24
|
+
respond.cancel("Autonomous mode. Make your own decision.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
var questions = planningQuestions(boundSession, request.questions);
|
|
28
|
+
var input = { questions: questions };
|
|
29
|
+
if (!boundSession.pendingAskUser) boundSession.pendingAskUser = {};
|
|
30
|
+
var pending = {
|
|
31
|
+
input: input,
|
|
32
|
+
mode: "yoke",
|
|
33
|
+
request: request,
|
|
34
|
+
respond: respond,
|
|
35
|
+
sessionId: boundSession.localId,
|
|
36
|
+
postedAt: Date.now(),
|
|
37
|
+
};
|
|
38
|
+
boundSession.pendingAskUser[request.id] = pending;
|
|
39
|
+
if (respond && typeof respond.onSettle === "function") {
|
|
40
|
+
respond.onSettle(function () {
|
|
41
|
+
if (boundSession.pendingAskUser && boundSession.pendingAskUser[request.id] === pending) delete boundSession.pendingAskUser[request.id];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
ctx.record(boundSession, { type: "tool_executing", id: request.id, name: "AskUserQuestion", input: input });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createHandler(boundSession) {
|
|
48
|
+
return function (request, respond) { handleRequest(boundSession, request, respond); };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createMcpServer(adapter, boundSession) {
|
|
52
|
+
return userInput.createFallbackServer(adapter, createHandler(boundSession), {
|
|
53
|
+
force: !!(boundSession && boundSession.debateSetupMode),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
createHandler: createHandler,
|
|
59
|
+
createMcpServer: createMcpServer,
|
|
60
|
+
getToolDefs: function (boundSession) { return userInput.fallbackToolDefs(createHandler(boundSession)); },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { attachAskUser: attachAskUser };
|