clay-server 4.0.0-beta.15 → 4.0.0-beta.16
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/capsule-frame-server.js +26 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +33 -1
- package/lib/capsule-server-runtimes.js +0 -2
- package/lib/capsules/pig/display.js +8 -0
- package/lib/capsules/pig/manifest.json +1 -1
- package/lib/capsules/pig/ui.json +52 -14
- package/lib/daemon.js +8 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-logs-mcp-server.js +11 -10
- package/lib/project-session-pair.js +15 -5
- package/lib/project-worker-proposal.js +8 -8
- package/lib/project.js +13 -1
- package/lib/public/app.js +6 -15
- package/lib/public/css/capsule-ui.css +21 -1
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sticky-notes.css +3 -7
- package/lib/public/index.html +31 -9
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +6 -3
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-tool-frame.js +17 -0
- package/lib/public/modules/home-tools.js +9 -0
- package/lib/public/modules/input.js +32 -6
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +2 -0
- package/lib/sdk-bridge.js +14 -7
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-home-chat-events.js +28 -1
- package/lib/server-home-chat.js +1 -1
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +30 -0
- package/lib/server.js +35 -2
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-pair-prompts.js +24 -5
- package/lib/tools-registry.js +46 -1
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
- package/lib/capsule-tictactoe-logic.js +0 -260
- package/lib/capsules/tictactoe/manifest.json +0 -9
- package/lib/capsules/tictactoe/ui.json +0 -203
|
@@ -132,8 +132,34 @@ function createCapsuleFrameServer(opts) {
|
|
|
132
132
|
" if (data.type === \"state\" && typeof api.onState === \"function\") api.onState(data.state || {});",
|
|
133
133
|
" else if (data.type === \"event\" && typeof api.onEvent === \"function\") api.onEvent(data.event || null);",
|
|
134
134
|
" });",
|
|
135
|
+
" // The host owns the iframe box, so the frame reports its natural",
|
|
136
|
+
" // content height and the host resizes the box to fit. Without this",
|
|
137
|
+
" // the box stays fixed and a widening layout clips the element.",
|
|
138
|
+
" var reportedHeight = 0;",
|
|
139
|
+
" var reportQueued = false;",
|
|
140
|
+
" function reportSize() {",
|
|
141
|
+
" reportQueued = false;",
|
|
142
|
+
" var root = document.documentElement;",
|
|
143
|
+
" var body = document.body;",
|
|
144
|
+
" var height = Math.ceil(Math.max(root ? root.scrollHeight : 0, body ? body.scrollHeight : 0));",
|
|
145
|
+
" if (!height || height === reportedHeight) return;",
|
|
146
|
+
" reportedHeight = height;",
|
|
147
|
+
" window.parent.postMessage({ clayCapsuleFrame: 1, type: \"size\", height: height }, \"*\");",
|
|
148
|
+
" }",
|
|
149
|
+
" function queueReport() {",
|
|
150
|
+
" if (reportQueued) return;",
|
|
151
|
+
" reportQueued = true;",
|
|
152
|
+
" if (typeof requestAnimationFrame === \"function\") requestAnimationFrame(reportSize);",
|
|
153
|
+
" else setTimeout(reportSize, 50);",
|
|
154
|
+
" }",
|
|
155
|
+
" if (typeof ResizeObserver === \"function\") {",
|
|
156
|
+
" new ResizeObserver(queueReport).observe(document.documentElement);",
|
|
157
|
+
" if (document.body) new ResizeObserver(queueReport).observe(document.body);",
|
|
158
|
+
" }",
|
|
159
|
+
" window.addEventListener(\"resize\", queueReport);",
|
|
135
160
|
" window.addEventListener(\"load\", function () {",
|
|
136
161
|
" window.parent.postMessage({ clayCapsuleFrame: 1, type: \"ready\" }, \"*\");",
|
|
162
|
+
" queueReport();",
|
|
137
163
|
" });",
|
|
138
164
|
"})();",
|
|
139
165
|
].join("\n");
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Bridges Capsule engagement events to the opponent Mate's session.
|
|
2
|
+
//
|
|
3
|
+
// This module knows no game. A Capsule's Logic declares on its causal event
|
|
4
|
+
// when the Mate should be engaged (`event.engage = {kind}`): "turn" asks the
|
|
5
|
+
// Mate to read the state and act, "start" asks it to acknowledge a fresh
|
|
6
|
+
// game without acting. The bridge only carries that declaration: it finds the
|
|
7
|
+
// opponent Mate's project and delivers a prompt through it, so the Capsule
|
|
8
|
+
// round-trips for every game alike: the human plays on the Display, the Mate
|
|
9
|
+
// wakes up, reads the state over clay_tool_snapshot, and acts through the
|
|
10
|
+
// same clay_tool_act pipeline. The prompt carries words only; it grants no
|
|
11
|
+
// authority the Mate does not already have, and a lost prompt costs a
|
|
12
|
+
// reminder, never the game. Only human-caused events engage, whatever Logic
|
|
13
|
+
// declares, so a Mate can never wake itself.
|
|
14
|
+
//
|
|
15
|
+
// The opponent is the Mate that last acted on this Capsule for this user.
|
|
16
|
+
// Before any Mate has acted, the built-in host Mate (Clay) takes the seat, so
|
|
17
|
+
// a fresh game is playable with zero setup.
|
|
18
|
+
|
|
19
|
+
function attachCapsuleMateTurn(deps) {
|
|
20
|
+
var users = deps.users;
|
|
21
|
+
var findMateProject = deps.findMateProject || null;
|
|
22
|
+
var broadcastToUser = deps.broadcastToUser || null;
|
|
23
|
+
var opponents = Object.create(null);
|
|
24
|
+
var nudgedSeqs = Object.create(null);
|
|
25
|
+
|
|
26
|
+
function keyFor(userId, toolId) {
|
|
27
|
+
return String(userId) + "\u0000" + String(toolId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Called when a Mate acts on a Capsule: that Mate holds the opponent seat
|
|
31
|
+
// for this user's game from then on.
|
|
32
|
+
function rememberOpponent(userId, toolId, mateId) {
|
|
33
|
+
if (typeof mateId === "string" && mateId) opponents[keyFor(userId, toolId)] = mateId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function turnPrompt(manifest) {
|
|
37
|
+
return "Capsule turn: it is your seat's turn in the \"" + manifest.name + "\" Capsule (toolId \"" + manifest.id + "\"). "
|
|
38
|
+
+ "Read the game with clay_tool_snapshot, decide your move, and take your turn with clay_tool_act. "
|
|
39
|
+
+ "Keep playing until the turn passes away from your seat, and say your reasoning in one or two short sentences as you play. "
|
|
40
|
+
+ "If an act is refused, read the snapshot again instead of retrying blindly.";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function startPrompt(manifest) {
|
|
44
|
+
return "Capsule game: the user just started a new game of \"" + manifest.name + "\" (toolId \"" + manifest.id + "\") against you. "
|
|
45
|
+
+ "The user moves first. Reply with one short line of table talk, and do not call clay_tool_act now: "
|
|
46
|
+
+ "you will be woken in this session whenever it is your turn.";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fire-and-forget from the act pipeline: delivers a Logic-declared
|
|
50
|
+
// engagement exactly once per event, and only for human-caused events.
|
|
51
|
+
function maybeNudge(userId, manifest, actor, event) {
|
|
52
|
+
if (!findMateProject || !manifest || !event) return Promise.resolve(null);
|
|
53
|
+
if (actor !== "human") return Promise.resolve(null);
|
|
54
|
+
var kind = event.engage && (event.engage.kind === "turn" || event.engage.kind === "start") ? event.engage.kind : null;
|
|
55
|
+
if (!kind) return Promise.resolve(null);
|
|
56
|
+
var key = keyFor(userId, manifest.id);
|
|
57
|
+
if (nudgedSeqs[key] !== undefined && event.seq <= nudgedSeqs[key]) return Promise.resolve(null);
|
|
58
|
+
nudgedSeqs[key] = event.seq;
|
|
59
|
+
return Promise.resolve().then(function () {
|
|
60
|
+
var mateId = opponents[key] || null;
|
|
61
|
+
// The mates registry keys single-user data off a null userId, exactly
|
|
62
|
+
// like the home chat surface does.
|
|
63
|
+
var mateUserId = users.isMultiUser() ? userId : null;
|
|
64
|
+
var found = findMateProject(mateUserId, mateId, true);
|
|
65
|
+
if (!found && mateId) found = findMateProject(mateUserId, null, true);
|
|
66
|
+
if (!found || !found.ctx || typeof found.ctx.deliverCapsuleTurn !== "function") {
|
|
67
|
+
console.error("[capsules] No Mate project could take the '" + manifest.id + "' turn; is a Mate installed?");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
var principal = { userId: users.isMultiUser() ? userId : null };
|
|
71
|
+
var mateId = found.mate && found.mate.id ? found.mate.id : null;
|
|
72
|
+
return found.ctx.deliverCapsuleTurn(principal, {
|
|
73
|
+
toolId: manifest.id,
|
|
74
|
+
toolName: manifest.name,
|
|
75
|
+
kind: kind,
|
|
76
|
+
text: kind === "turn" ? turnPrompt(manifest) : startPrompt(manifest),
|
|
77
|
+
}).then(function (delivered) {
|
|
78
|
+
// An explicit game start navigates the user's home board into the
|
|
79
|
+
// Mate's game session, so starting a game visibly opens the table.
|
|
80
|
+
if (broadcastToUser && delivered && delivered.reference && mateId) {
|
|
81
|
+
broadcastToUser(userId, {
|
|
82
|
+
type: "capsule_game_session",
|
|
83
|
+
toolId: manifest.id,
|
|
84
|
+
toolName: manifest.name,
|
|
85
|
+
mateId: mateId,
|
|
86
|
+
sessionId: delivered.reference,
|
|
87
|
+
kind: kind,
|
|
88
|
+
created: !!delivered.created,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return delivered;
|
|
92
|
+
});
|
|
93
|
+
}).catch(function (error) {
|
|
94
|
+
console.error("[capsules] Could not deliver the Mate's turn:", error && error.message ? error.message : error);
|
|
95
|
+
return null;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
rememberOpponent: rememberOpponent,
|
|
101
|
+
maybeNudge: maybeNudge,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { attachCapsuleMateTurn: attachCapsuleMateTurn };
|
package/lib/capsule-pig-logic.js
CHANGED
|
@@ -159,6 +159,18 @@ function reset(state, seat) {
|
|
|
159
159
|
return newGame();
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function statusTextFor(state, complete, userTurn) {
|
|
163
|
+
if (complete) {
|
|
164
|
+
return state.winner === USER ? "You win this game!" : "Your Mate wins this game.";
|
|
165
|
+
}
|
|
166
|
+
if (userTurn) {
|
|
167
|
+
return state.turnTotal > 0
|
|
168
|
+
? "Your turn: roll again or hold to bank " + state.turnTotal + " point(s)."
|
|
169
|
+
: "Your turn: roll the die.";
|
|
170
|
+
}
|
|
171
|
+
return "Your Mate's turn. Moves appear here as they happen.";
|
|
172
|
+
}
|
|
173
|
+
|
|
162
174
|
// The projection a Display renders and a Mate reads. It adds no meaning that is
|
|
163
175
|
// absent from state: every derived field is a restatement of the fields above.
|
|
164
176
|
// The score series exist because a progress chart binds to a collection.
|
|
@@ -172,7 +184,12 @@ function project(state) {
|
|
|
172
184
|
userScoreSeries: [{ seat: SEAT_LABELS.user, value: state.scores.user }],
|
|
173
185
|
mateScoreSeries: [{ seat: SEAT_LABELS.mate, value: state.scores.mate }],
|
|
174
186
|
turnTotal: state.turnTotal,
|
|
187
|
+
turnTotalSeries: [{ label: "Turn total", value: state.turnTotal }],
|
|
175
188
|
turnTotalText: "Turn total " + state.turnTotal + " of " + state.target + ", " + SEAT_LABELS[state.turn] + " to play.",
|
|
189
|
+
statusText: statusTextFor(state, complete, userTurn),
|
|
190
|
+
lastRollText: state.lastRoll === null
|
|
191
|
+
? "No rolls yet."
|
|
192
|
+
: "Last roll: " + state.lastRoll + " by " + SEAT_LABELS[state.lastActor || state.turn] + ".",
|
|
176
193
|
lastRoll: state.lastRoll,
|
|
177
194
|
lastActor: state.lastActor,
|
|
178
195
|
winner: state.winner,
|
|
@@ -184,17 +201,32 @@ function project(state) {
|
|
|
184
201
|
};
|
|
185
202
|
}
|
|
186
203
|
|
|
204
|
+
// When the Mate should be engaged is a game rule, so Logic declares it on the
|
|
205
|
+
// event rather than the host inferring it from Pig's fields. The host bridge
|
|
206
|
+
// delivers any declared engagement generically for every Capsule: "turn"
|
|
207
|
+
// means play your seat now, "start" means the human opened a fresh game and
|
|
208
|
+
// you should acknowledge it without acting.
|
|
209
|
+
function engagementFor(seat, actionId, next) {
|
|
210
|
+
if (seat !== USER) return null;
|
|
211
|
+
if (next.status === "playing" && next.turn === MATE) return { kind: "turn" };
|
|
212
|
+
if (actionId === "reset" && next.status === "playing") return { kind: "start" };
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
187
216
|
// Causality for the live Display: {actor, action, previous, next} plus a
|
|
188
217
|
// monotonic seq. Sending only the new state would make a Mate's move teleport
|
|
189
218
|
// onto the human's screen; the event lets the Display replay and attribute it.
|
|
190
219
|
function buildEvent(seat, actionId, previous, next) {
|
|
191
|
-
|
|
220
|
+
var event = {
|
|
192
221
|
seq: next.eventSeq,
|
|
193
222
|
actor: seat,
|
|
194
223
|
action: actionId,
|
|
195
224
|
previous: previous,
|
|
196
225
|
next: next,
|
|
197
226
|
};
|
|
227
|
+
var engage = engagementFor(seat, actionId, next);
|
|
228
|
+
if (engage) event.engage = engage;
|
|
229
|
+
return event;
|
|
198
230
|
}
|
|
199
231
|
|
|
200
232
|
function seatFor(context) {
|
|
@@ -13,11 +13,9 @@ var path = require("path");
|
|
|
13
13
|
var toolStorage = require("./tool-storage");
|
|
14
14
|
var toolsRegistry = require("./tools-registry");
|
|
15
15
|
var pigLogic = require("./capsule-pig-logic");
|
|
16
|
-
var tictactoeLogic = require("./capsule-tictactoe-logic");
|
|
17
16
|
|
|
18
17
|
var RUNTIME_FACTORIES = {
|
|
19
18
|
pig: pigLogic.createRuntime,
|
|
20
|
-
tictactoe: tictactoeLogic.createRuntime,
|
|
21
19
|
};
|
|
22
20
|
|
|
23
21
|
function hasRuntime(toolId) {
|
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
controls.style.marginTop = "10px";
|
|
43
43
|
root.appendChild(controls);
|
|
44
44
|
|
|
45
|
+
var rulesEl = document.createElement("p");
|
|
46
|
+
rulesEl.textContent = "Pig is a push-your-luck dice game: roll to build this turn's total, but a 1 loses it. Hold to bank your points; the first player to 100 wins.";
|
|
47
|
+
rulesEl.style.margin = "12px 0 0";
|
|
48
|
+
rulesEl.style.color = "#b8b8b8";
|
|
49
|
+
rulesEl.style.fontSize = "12px";
|
|
50
|
+
rulesEl.style.lineHeight = "1.45";
|
|
51
|
+
root.appendChild(rulesEl);
|
|
52
|
+
|
|
45
53
|
function makeButton(label, action, accent) {
|
|
46
54
|
var button = document.createElement("button");
|
|
47
55
|
button.type = "button";
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
"lucideIcon": "sparkles",
|
|
6
6
|
"description": "Two-player push-your-luck dice game shared by the user and one Mate.",
|
|
7
7
|
"useWhen": "Use when the user wants to play Pig, take a turn, or hear how the current game stands.",
|
|
8
|
-
"skills": "# Pig\n\nA two-seat push-your-luck dice game. The seats are `user` and `mate`; the person holds the user seat and you hold the mate seat. All randomness, turn order, and scoring are enforced server-side by the Capsule's Logic; you cannot roll a die yourself, and neither can the user.\n\n## Rules\n\n- Seats alternate turns. On your turn you may roll as often as you like.\n- Each roll adds its face value to the turn total. Rolling a 1 loses the entire turn total and passes play to the other seat.\n- Holding banks the turn total into your score and passes play.\n- The first seat to bank at least 100 points wins. A finished game is frozen until it is reset.\n- Only the user may reset a game that is still in progress. You may reset once the game is complete.\n\n## How to play\n\n1. `clay_tool_snapshot` with toolId `pig` to read the current state: `turn`, `scores.user`, `scores.mate`, `turnTotal`, `status`, `winner`, and `recentRolls`, which lists recent rolls only and never holds or wins.\n2. Act only when `turn` is `mate`. `clay_tool_act` with actionId `roll`, `hold`, or `reset`. Arguments are not used.\n3. An out-of-turn or post-game call is refused by Logic with an explanatory error. Read the snapshot again rather than retrying blindly.\n4. Every act returns the complete new state, so a snapshot immediately after an act is redundant.\n\n## Playing well\n\nThe interesting part of this Capsule is the judgement call, not the mechanics: decide whether to roll again or hold, and say why in your own words before you act. Typical play holds at a turn total near 20, but the score gap matters more than the rule of thumb, so explain the trade-off you are taking. Never claim a roll result before the act returns it."
|
|
8
|
+
"skills": "# Pig\n\nA two-seat push-your-luck dice game. The seats are `user` and `mate`; the person holds the user seat and you hold the mate seat. All randomness, turn order, and scoring are enforced server-side by the Capsule's Logic; you cannot roll a die yourself, and neither can the user.\n\n## Rules\n\n- Seats alternate turns. On your turn you may roll as often as you like.\n- Each roll adds its face value to the turn total. Rolling a 1 loses the entire turn total and passes play to the other seat.\n- Holding banks the turn total into your score and passes play.\n- The first seat to bank at least 100 points wins. A finished game is frozen until it is reset.\n- Only the user may reset a game that is still in progress. You may reset once the game is complete.\n\n## How to play\n\n1. `clay_tool_snapshot` with toolId `pig` to read the current state: `turn`, `scores.user`, `scores.mate`, `turnTotal`, `status`, `winner`, and `recentRolls`, which lists recent rolls only and never holds or wins.\n2. Act only when `turn` is `mate`. `clay_tool_act` with actionId `roll`, `hold`, or `reset`. Arguments are not used.\n3. An out-of-turn or post-game call is refused by Logic with an explanatory error. Read the snapshot again rather than retrying blindly.\n4. Every act returns the complete new state, so a snapshot immediately after an act is redundant.\n\n## Playing well\n\nThe interesting part of this Capsule is the judgement call, not the mechanics: decide whether to roll again or hold, and say why in your own words before you act. Typical play holds at a turn total near 20, but the score gap matters more than the rule of thumb, so explain the trade-off you are taking. Never claim a roll result before the act returns it.\n\n## Turn flow\n\nWhen the user's move hands the turn to your seat, Clay wakes you in a dedicated game session with a \"Capsule turn\" message. Take your whole turn there: roll as often as you judge right, then hold (or bust), and stop once the turn passes away from your seat. The user's Display updates live as you act, so never narrate moves you have not made. If nothing wakes you, you are not on turn."
|
|
9
9
|
}
|
package/lib/capsules/pig/ui.json
CHANGED
|
@@ -3,16 +3,42 @@
|
|
|
3
3
|
"props": { "gap": "md" },
|
|
4
4
|
"children": [
|
|
5
5
|
{
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
"props": { "
|
|
6
|
+
"type": "callout",
|
|
7
|
+
"when": { "equals": { "path": "$state.complete", "value": true } },
|
|
8
|
+
"props": { "title": "Game over", "text": { "$bind": "$state.statusText", "fallback": "Game over." }, "icon": "circle-check", "variant": "soft" },
|
|
9
|
+
"else": {
|
|
10
|
+
"type": "callout",
|
|
11
|
+
"props": { "title": "Pig", "text": { "$bind": "$state.statusText", "fallback": "Your turn: roll the die." }, "icon": "sparkles", "variant": "soft" }
|
|
12
|
+
}
|
|
9
13
|
},
|
|
10
14
|
{
|
|
11
|
-
"type": "
|
|
12
|
-
"
|
|
13
|
-
"
|
|
15
|
+
"type": "row",
|
|
16
|
+
"props": { "gap": "md", "wrap": true },
|
|
17
|
+
"children": [
|
|
18
|
+
{
|
|
19
|
+
"type": "chart",
|
|
20
|
+
"bind": "userScoreSeries",
|
|
21
|
+
"props": { "label": "Your score", "kind": "progress", "categoryKey": "seat", "valueKey": "value", "max": 100 }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "chart",
|
|
25
|
+
"bind": "mateScoreSeries",
|
|
26
|
+
"props": { "label": "Your Mate's score", "kind": "progress", "categoryKey": "seat", "valueKey": "value", "max": 100 }
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "row",
|
|
32
|
+
"props": { "gap": "md", "align": "center", "wrap": true },
|
|
33
|
+
"children": [
|
|
34
|
+
{
|
|
35
|
+
"type": "chart",
|
|
36
|
+
"bind": "turnTotalSeries",
|
|
37
|
+
"props": { "label": "Turn total", "kind": "metric", "valueKey": "value" }
|
|
38
|
+
},
|
|
39
|
+
{ "type": "text", "props": { "text": { "$bind": "$state.lastRollText", "fallback": "No rolls yet." }, "role": "muted" } }
|
|
40
|
+
]
|
|
14
41
|
},
|
|
15
|
-
{ "type": "text", "props": { "text": { "$bind": "$state.turnTotalText", "fallback": "Turn total 0 of 100." }, "role": "output" } },
|
|
16
42
|
{
|
|
17
43
|
"type": "row",
|
|
18
44
|
"props": { "gap": "sm", "wrap": true },
|
|
@@ -20,26 +46,38 @@
|
|
|
20
46
|
{
|
|
21
47
|
"type": "button",
|
|
22
48
|
"action": "roll",
|
|
23
|
-
"when": { "
|
|
49
|
+
"when": { "equals": { "path": "$state.userTurn", "value": true } },
|
|
24
50
|
"props": { "label": "Roll", "variant": "primary" },
|
|
25
51
|
"else": { "type": "button", "action": "roll", "props": { "label": "Roll", "variant": "primary", "disabled": true } }
|
|
26
52
|
},
|
|
27
53
|
{
|
|
28
54
|
"type": "button",
|
|
29
55
|
"action": "hold",
|
|
30
|
-
"when": { "
|
|
56
|
+
"when": { "equals": { "path": "$state.userTurn", "value": true } },
|
|
31
57
|
"props": { "label": "Hold", "variant": "secondary" },
|
|
32
58
|
"else": { "type": "button", "action": "hold", "props": { "label": "Hold", "variant": "secondary", "disabled": true } }
|
|
33
59
|
},
|
|
34
|
-
{
|
|
60
|
+
{
|
|
61
|
+
"type": "button",
|
|
62
|
+
"action": "reset",
|
|
63
|
+
"when": { "equals": { "path": "$state.complete", "value": true } },
|
|
64
|
+
"props": { "label": "New game", "variant": "primary" },
|
|
65
|
+
"else": { "type": "button", "action": "reset", "props": { "label": "Start over", "variant": "ghost" } }
|
|
66
|
+
}
|
|
35
67
|
]
|
|
36
68
|
},
|
|
37
69
|
{
|
|
38
|
-
"type": "
|
|
39
|
-
"
|
|
40
|
-
"props": { "variant": "divided", "gap": "xs" },
|
|
70
|
+
"type": "section",
|
|
71
|
+
"props": { "label": "Recent rolls", "variant": "inset", "gap": "xs" },
|
|
41
72
|
"children": [
|
|
42
|
-
{
|
|
73
|
+
{
|
|
74
|
+
"type": "list",
|
|
75
|
+
"bind": "recentRolls",
|
|
76
|
+
"props": { "variant": "divided", "gap": "xs" },
|
|
77
|
+
"children": [
|
|
78
|
+
{ "type": "text", "props": { "text": "$item.text", "role": "body" } }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
43
81
|
]
|
|
44
82
|
}
|
|
45
83
|
]
|
package/lib/daemon.js
CHANGED
|
@@ -740,6 +740,7 @@ var relay = createServer({
|
|
|
740
740
|
inheritGroups: config.inheritGroups !== false,
|
|
741
741
|
chatLayout: config.chatLayout || "channel",
|
|
742
742
|
matesEnabled: config.matesEnabled === true,
|
|
743
|
+
capsulesEnabled: config.capsulesEnabled === true,
|
|
743
744
|
pinEnabled: !!config.pinHash,
|
|
744
745
|
platform: process.platform,
|
|
745
746
|
hostname: os2.hostname(),
|
|
@@ -818,6 +819,13 @@ var relay = createServer({
|
|
|
818
819
|
console.log("[daemon] Project Mate DMs:", want ? "shown" : "hidden", "(web)");
|
|
819
820
|
return { ok: true, matesEnabled: want };
|
|
820
821
|
},
|
|
822
|
+
onSetCapsulesEnabled: function (value) {
|
|
823
|
+
var want = value === true;
|
|
824
|
+
config.capsulesEnabled = want;
|
|
825
|
+
saveConfig(config);
|
|
826
|
+
console.log("[daemon] Experimental Capsules:", want ? "enabled" : "disabled", "(web)");
|
|
827
|
+
return { ok: true, capsulesEnabled: want };
|
|
828
|
+
},
|
|
821
829
|
onGetToolPalettes: function () {
|
|
822
830
|
return config.toolPalettes || {};
|
|
823
831
|
},
|
|
@@ -23,6 +23,7 @@ var SETTING_KEYS = [
|
|
|
23
23
|
"chatLayout",
|
|
24
24
|
"autoContinueOnRateLimit",
|
|
25
25
|
"matesEnabled",
|
|
26
|
+
"capsulesEnabled",
|
|
26
27
|
"terminalFont",
|
|
27
28
|
"deletedBuiltinKeys",
|
|
28
29
|
"mateOnboardingShown",
|
|
@@ -189,6 +190,7 @@ function migrateSingleUserToMulti() {
|
|
|
189
190
|
if (cfg.chatLayout !== undefined && users.setChatLayout) users.setChatLayout(userId, cfg.chatLayout);
|
|
190
191
|
if (cfg.autoContinueOnRateLimit !== undefined && users.setAutoContinue) users.setAutoContinue(userId, cfg.autoContinueOnRateLimit);
|
|
191
192
|
if (cfg.matesEnabled !== undefined && users.setMatesEnabled) users.setMatesEnabled(userId, cfg.matesEnabled);
|
|
193
|
+
if (cfg.capsulesEnabled !== undefined && users.setCapsulesEnabled) users.setCapsulesEnabled(userId, cfg.capsulesEnabled);
|
|
192
194
|
if (cfg.terminalFont && users.setTerminalFont) {
|
|
193
195
|
users.setTerminalFont(userId, cfg.terminalFont.family, cfg.terminalFont.size);
|
|
194
196
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Delivers a Capsule turn prompt into this Mate project.
|
|
2
|
+
//
|
|
3
|
+
// A Capsule game against a Mate needs the Mate to actually take its turns:
|
|
4
|
+
// when Logic passes the turn to the mate seat, this module wakes the Mate
|
|
5
|
+
// with a prompt telling it to snapshot the game and act. One session hosts
|
|
6
|
+
// the whole game (found again by session.capsuleGame.toolId), so the user
|
|
7
|
+
// watches a single running conversation of the Mate playing rather than a
|
|
8
|
+
// new session per turn. The prompt is recorded as an internal user message,
|
|
9
|
+
// the same shape the debate engine uses, so the transcript shows the nudge
|
|
10
|
+
// without pretending the human typed it.
|
|
11
|
+
//
|
|
12
|
+
// This module only carries words to the Mate. Every actual game mutation the
|
|
13
|
+
// Mate then makes goes through the same clay_tool_act pipeline as always.
|
|
14
|
+
|
|
15
|
+
function attachCapsuleTurn(ctx) {
|
|
16
|
+
var sm = ctx.sm;
|
|
17
|
+
|
|
18
|
+
function ownsSession(session, principal) {
|
|
19
|
+
if (ctx.isMultiUser()) return !!principal.userId && session.ownerId === principal.userId;
|
|
20
|
+
return !session.ownerId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findGameSession(principal, toolId) {
|
|
24
|
+
var found = null;
|
|
25
|
+
sm.sessions.forEach(function (session) {
|
|
26
|
+
if (found || !session || session.hidden || session.destroying) return;
|
|
27
|
+
if (!session.capsuleGame || session.capsuleGame.toolId !== toolId) return;
|
|
28
|
+
if (!ownsSession(session, principal)) return;
|
|
29
|
+
found = session;
|
|
30
|
+
});
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function createGameSession(principal, options) {
|
|
35
|
+
var selection = await ctx.resolveModel(principal);
|
|
36
|
+
if (!selection || selection.status !== "ready" || !selection.vendor || !selection.model) {
|
|
37
|
+
throw new Error(selection && selection.error ? selection.error : "No configured model is available for the Mate.");
|
|
38
|
+
}
|
|
39
|
+
var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
|
|
40
|
+
var session = create.call(sm, {
|
|
41
|
+
ownerId: principal.userId || null,
|
|
42
|
+
vendor: selection.vendor,
|
|
43
|
+
model: selection.model,
|
|
44
|
+
});
|
|
45
|
+
session.title = (options.toolName || options.toolId) + " game";
|
|
46
|
+
session.capsuleGame = { toolId: options.toolId };
|
|
47
|
+
return session;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function deliverCapsuleTurn(principal, options) {
|
|
51
|
+
if (!principal || !options || !options.toolId || !options.text) {
|
|
52
|
+
throw new Error("A bound owner, toolId, and turn text are required.");
|
|
53
|
+
}
|
|
54
|
+
var sdk = ctx.getSdk();
|
|
55
|
+
if (!sdk || typeof sdk.startQuery !== "function") throw new Error("Mate project runtime is unavailable.");
|
|
56
|
+
var session = findGameSession(principal, options.toolId);
|
|
57
|
+
var created = false;
|
|
58
|
+
if (!session) {
|
|
59
|
+
session = await createGameSession(principal, options);
|
|
60
|
+
created = true;
|
|
61
|
+
}
|
|
62
|
+
// Recorded as a dedicated event, not a user message: the transcript shows
|
|
63
|
+
// a short system note (see historyToHomeChat) instead of pretending the
|
|
64
|
+
// human typed the delivery prompt. The full prompt still reaches the
|
|
65
|
+
// model through the query below.
|
|
66
|
+
sm.sendAndRecord(session, {
|
|
67
|
+
type: "capsule_turn",
|
|
68
|
+
text: options.text,
|
|
69
|
+
toolId: options.toolId,
|
|
70
|
+
toolName: options.toolName || options.toolId,
|
|
71
|
+
kind: options.kind || "turn",
|
|
72
|
+
});
|
|
73
|
+
session.isProcessing = true;
|
|
74
|
+
session.lastActivity = Date.now();
|
|
75
|
+
sm.broadcastSessionList();
|
|
76
|
+
try {
|
|
77
|
+
var delivered = typeof sdk.pushMessage === "function" ? sdk.pushMessage(session, options.text) : false;
|
|
78
|
+
if (!delivered) await Promise.resolve(sdk.startQuery(session, options.text, undefined, ctx.getLinuxUserForSession(session)));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
session.isProcessing = false;
|
|
81
|
+
sm.sendAndRecord(session, { type: "error", text: "Capsule turn could not start: " + (error && error.message ? error.message : String(error)) });
|
|
82
|
+
sm.broadcastSessionList();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
// The reference shape the home chat surface opens sessions by.
|
|
86
|
+
return {
|
|
87
|
+
session: session,
|
|
88
|
+
created: created,
|
|
89
|
+
reference: (typeof session.cliSessionId === "string" && session.cliSessionId) || "local:" + session.localId,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { deliverCapsuleTurn: deliverCapsuleTurn };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { attachCapsuleTurn: attachCapsuleTurn };
|
|
@@ -12,14 +12,15 @@ var logsSchema = require("./project-logs-schema");
|
|
|
12
12
|
var logsComments = require("./project-logs-comments");
|
|
13
13
|
|
|
14
14
|
var LOGS_CONTRACT =
|
|
15
|
-
"Project Logs are this project's durable record
|
|
15
|
+
"Project Logs are this project's durable work-continuity record, written so a newly created Driver can understand what the user asked for, what happened, and what remains without reading the previous chat. " +
|
|
16
16
|
"You are the only author: connected people read the log and may add comments, but they cannot create or revise entries. That makes accuracy your responsibility. " +
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"Log every concrete user work instruction that changes, diagnoses, designs, or verifies project state, not only unusually important work. Create or identify one entry for the coherent task, then revise that same entry as work progresses instead of creating one entry per turn. " +
|
|
18
|
+
"The entry must preserve the user's requested outcome and material constraints, followed by what was changed, discovered, or decided, the affected area, verification, and the current result. If work is incomplete or blocked, state the remaining work and next action explicitly. For a small task, one completed entry is enough; for longer work, create it when the task starts and update it at meaningful milestones and completion. " +
|
|
19
|
+
"Every entry needs a concise meaningful title, a one or two sentence summary that combines the request with the current outcome, and a category. Set priority when an entry genuinely outranks routine work; routine work still belongs in the ledger at normal priority. " +
|
|
19
20
|
"Categories are this project's own evolving vocabulary rather than a fixed list: list or search first, reuse an established category when one fits, and coin a new concise one only when the project needs a durable distinction it lacks. " +
|
|
20
21
|
"Prefer updating an existing entry over creating a near-duplicate: when a decision supersedes an earlier one, revise that entry so its history shows the change. " +
|
|
21
|
-
"Do not
|
|
22
|
-
"Every write is attributed and permanently revision-tracked, so keep entries
|
|
22
|
+
"Do not paste raw conversation transcripts, log command-by-command narration, trivial confirmations, or speculation. Repository history may show the code change but usually does not preserve the user's intent, constraints, verification, or unfinished state, so it is not a substitute for the work log. " +
|
|
23
|
+
"Every write is attributed and permanently revision-tracked, so keep entries concise, concrete, and true while retaining enough context for a clean Driver handoff.";
|
|
23
24
|
|
|
24
25
|
// User learning moments are a durable project asset, so capturing them is a
|
|
25
26
|
// default rather than an option. This category is about a change in the user's
|
|
@@ -75,7 +76,7 @@ var CATEGORY_DESCRIPTION = "Record category: a short lowercase hyphen-separated
|
|
|
75
76
|
"Common starting points are " + logsSchema.SEED_CATEGORIES.join(", ") + ". A category is dry metadata, never a persona or an identifier. " +
|
|
76
77
|
"Use `learning` only for a user learning moment described by the learning contract, never for knowledge or lessons acquired by the Driver.";
|
|
77
78
|
var PRIORITY_DESCRIPTION = "How much this outranks routine work: " + logsSchema.PRIORITIES.join(", ") + ". Defaults to normal. Priority is independent of category, so an urgent decision is both.";
|
|
78
|
-
var SUMMARY_DESCRIPTION = "One or two sentences
|
|
79
|
+
var SUMMARY_DESCRIPTION = "One or two sentences combining the user's requested outcome with the current result or status. This is what a new Driver sees in the ledger, so it must stand alone. For a learning entry, identify the concept the user engaged with plainly enough that the row itself teaches it.";
|
|
79
80
|
var REF_DESCRIPTION = "Opaque log reference returned by list_logs, search_logs, or create_log.";
|
|
80
81
|
|
|
81
82
|
function textResult(value) {
|
|
@@ -148,27 +149,27 @@ function projectTools(bound) {
|
|
|
148
149
|
},
|
|
149
150
|
{
|
|
150
151
|
name: "create_log",
|
|
151
|
-
description: LOGS_CONTRACT + " " + LEARNING_CONTRACT + " Create a log entry
|
|
152
|
+
description: LOGS_CONTRACT + " " + LEARNING_CONTRACT + " Create a log entry for a new coherent user-directed task or durable project record. Search first; if the task already has an entry, revise it instead of adding a duplicate.",
|
|
152
153
|
inputSchema: buildShape({
|
|
153
154
|
kind: { type: "string", description: CATEGORY_DESCRIPTION },
|
|
154
155
|
priority: { type: "string", enum: logsSchema.PRIORITIES, description: PRIORITY_DESCRIPTION },
|
|
155
156
|
title: { type: "string", description: "Short factual title, plain text, written like a good commit subject." },
|
|
156
157
|
summary: { type: "string", description: SUMMARY_DESCRIPTION },
|
|
157
|
-
body: { type: "string", description: "The
|
|
158
|
+
body: { type: "string", description: "The concise continuity record in Markdown: requested outcome and constraints, work/result, affected area, verification, current status, and next action when unfinished. Omit raw transcripts and command-by-command narration. For a learning entry, cover the user's original wording or mental model, the precise concept, how it applies here, and any boundary or misconception." },
|
|
158
159
|
tags: { type: "string", description: "Optional JSON array of short tag strings." },
|
|
159
160
|
}, ["kind", "title", "summary"]),
|
|
160
161
|
handler: handler(bound, "createLog"),
|
|
161
162
|
},
|
|
162
163
|
{
|
|
163
164
|
name: "update_log",
|
|
164
|
-
description: LOGS_CONTRACT + " " + LEARNING_CONTRACT + " Revise
|
|
165
|
+
description: LOGS_CONTRACT + " " + LEARNING_CONTRACT + " Revise the coherent task entry when work progresses, completes, becomes blocked, or a later decision supersedes it. Also revise when new learning refines an existing learning entry. The previous revision, its title, and its summary are all retained in the entry history.",
|
|
165
166
|
inputSchema: buildShape({
|
|
166
167
|
ref: { type: "string", description: REF_DESCRIPTION },
|
|
167
168
|
kind: { type: "string", description: CATEGORY_DESCRIPTION },
|
|
168
169
|
priority: { type: "string", enum: logsSchema.PRIORITIES, description: PRIORITY_DESCRIPTION },
|
|
169
170
|
title: { type: "string", description: "Replacement title. The previous title stays in the entry's history." },
|
|
170
171
|
summary: { type: "string", description: "Replacement summary. The previous summary stays in the entry's history." },
|
|
171
|
-
body: { type: "string", description: "Replacement record body in Markdown." },
|
|
172
|
+
body: { type: "string", description: "Replacement continuity record body in Markdown, including the request, current result, verification, and any remaining next action." },
|
|
172
173
|
tags: { type: "string", description: "Optional JSON array of short tag strings, replacing the current tags." },
|
|
173
174
|
}, ["ref"]),
|
|
174
175
|
handler: handler(bound, "updateLog"),
|
|
@@ -3,7 +3,7 @@ var pairPrompts = require("./session-pair-prompts");
|
|
|
3
3
|
var { attachPairFactory } = require("./session-pair-factory");
|
|
4
4
|
var { attachPairLifecycle } = require("./project-pair-lifecycle");
|
|
5
5
|
var { attachPairTurnControl } = require("./session-pair-turn-control");
|
|
6
|
-
var driverEligibility = require("./session-driver-eligibility");
|
|
6
|
+
var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration");
|
|
7
7
|
var { attachWorkerPermission } = require("./project-worker-permission");
|
|
8
8
|
var { attachWorkerProposal } = require("./project-worker-proposal");
|
|
9
9
|
var MAX_RESPONSE_CHARS = 30000;
|
|
@@ -372,7 +372,8 @@ function attachSessionPair(ctx) {
|
|
|
372
372
|
if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
|
|
373
373
|
var adHocSplit = !!(group && !group.pair);
|
|
374
374
|
if (!driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
|
|
375
|
-
|
|
375
|
+
var persistentCatalog = boundSession.vendor === "codex" && (!group || !!group.pair);
|
|
376
|
+
if (!group && !persistentCatalog) {
|
|
376
377
|
return workerProposal.getToolDefs(boundSession)
|
|
377
378
|
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
|
|
378
379
|
}
|
|
@@ -393,7 +394,12 @@ function attachSessionPair(ctx) {
|
|
|
393
394
|
});
|
|
394
395
|
},
|
|
395
396
|
evaluate: lifecycleHandlers.evaluate,
|
|
396
|
-
}, { lifecycle: !adHocSplit });
|
|
397
|
+
}, { lifecycle: persistentCatalog || !adHocSplit });
|
|
398
|
+
if (persistentCatalog) {
|
|
399
|
+
return workerProposal.getToolDefs(boundSession, { persistent: true })
|
|
400
|
+
.concat(tools)
|
|
401
|
+
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
|
|
402
|
+
}
|
|
397
403
|
return tools
|
|
398
404
|
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: false }));
|
|
399
405
|
}
|
|
@@ -466,8 +472,12 @@ function attachSessionPair(ctx) {
|
|
|
466
472
|
var group = store.groupForMember(session.localId);
|
|
467
473
|
var pairPrompt = "";
|
|
468
474
|
if (group && group.pair && group.pair.driverId === session.localId) {
|
|
469
|
-
pairPrompt = pairPrompts.
|
|
470
|
-
|
|
475
|
+
pairPrompt = pairPrompts.DRIVER_CORE;
|
|
476
|
+
if (driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
477
|
+
pairPrompt += " " + pairPrompts.DRIVER_DELEGATION;
|
|
478
|
+
}
|
|
479
|
+
} else if (!group && !ctx.isMate && driverEligibility.isEligibleDriverSession(session, sm) &&
|
|
480
|
+
driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
471
481
|
pairPrompt = pairPrompts.UNPAIRED + " " + workerProposal.getSystemPrompt(session);
|
|
472
482
|
}
|
|
473
483
|
return pairPrompt;
|