@stage5/lumine 0.2.50 → 0.2.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- package/lib/admin.js +112 -3
- package/lib/commands.js +4 -2
- package/lib/constants.js +4 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +94 -47
- package/sdk/LUMINE_ADMIN.md +46 -14
package/README.md
CHANGED
|
@@ -267,7 +267,8 @@ lumine admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3
|
|
|
267
267
|
lumine admin post reward comment:456 --twinkles 3 --json
|
|
268
268
|
lumine admin post skip-batch --target-file skipped.json --checkpoint skip-progress.json --json
|
|
269
269
|
lumine admin comment draft build:884 --file comment.md \
|
|
270
|
-
--review-receipt /path/from-build-review/review.json
|
|
270
|
+
--review-receipt /path/from-build-review/review.json \
|
|
271
|
+
--review-context build-context.json --json
|
|
271
272
|
lumine admin comment post --draft-id 77 --json
|
|
272
273
|
lumine admin news claim --output claim.json --scaffold editorial.json --json
|
|
273
274
|
lumine admin news validate --claim claim.json --file editorial.json --json
|
|
@@ -285,9 +286,9 @@ Numeric recommendation targets default to subjects. Use `comment:<id>`,
|
|
|
285
286
|
`aiStory:<id>`, `dailyReflection:<id>`, a canonical URL, or the matching
|
|
286
287
|
`--type` for another post kind. Commenting is always `off` for a new run unless
|
|
287
288
|
`--comment-mode draft` or `--comment-mode post` is explicitly supplied. Drafts
|
|
288
|
-
and posts use the selected bot's server-owned canonical persona
|
|
289
|
-
reply is handled by Twinkle's existing autonomous
|
|
290
|
-
Lumine remaining active.
|
|
289
|
+
and posts use the selected bot's server-owned canonical persona. Outside Build
|
|
290
|
+
threads, a later human reply is handled by Twinkle's existing autonomous
|
|
291
|
+
Zero/Ciel responder without Lumine remaining active.
|
|
291
292
|
|
|
292
293
|
Management agents also inspect recent public Build candidates during each run.
|
|
293
294
|
`builds review` opens one published app in an isolated temporary Chromium
|
|
@@ -297,8 +298,23 @@ subdirectory. A direct Build comment is never server-generated: review the
|
|
|
297
298
|
runtime (or pull and read an
|
|
298
299
|
open-source app), compose with `--file`, and attach the receipt. The server
|
|
299
300
|
rejects missing or stale review evidence and any app/thread change before
|
|
300
|
-
publication.
|
|
301
|
-
|
|
301
|
+
publication. Also pass `--review-context` with a private JSON file containing
|
|
302
|
+
only an `understanding` string: the concrete app behavior and design the agent
|
|
303
|
+
actually learned during that review. The server stamps the canonical Build,
|
|
304
|
+
published version, review method, and review time around that understanding;
|
|
305
|
+
none of it is exposed in the public comment payload. Manual
|
|
306
|
+
`--reviewed-version` / `--reviewed-via` evidence remains available for genuine
|
|
307
|
+
code reviews.
|
|
308
|
+
|
|
309
|
+
When a human directly replies to that management comment, or to a later
|
|
310
|
+
Zero/Ciel reply descended from it, the same bot may answer from the stored
|
|
311
|
+
historical understanding. That answer uses the commenter's normal AI Energy
|
|
312
|
+
path, including the usual reply-or-Like decision; if their battery is empty,
|
|
313
|
+
the ordinary sponsor placeholder and button are shown. Build mentions and
|
|
314
|
+
replies without this private management provenance remain disabled. A newer
|
|
315
|
+
published Build version does not rewrite history: the bot is told that its
|
|
316
|
+
understanding came from the older reviewed version and must not claim it
|
|
317
|
+
rechecked the app.
|
|
302
318
|
|
|
303
319
|
Every operation is noninteractive when its required arguments are present.
|
|
304
320
|
`--json` prints exactly one uncolored JSON value and returns a nonzero status
|
package/lib/admin.js
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
const MAX_EDITORIAL_FILE_BYTES = 256 * 1024;
|
|
18
18
|
const MAX_COMPOSED_TEXT_FILE_BYTES = 64 * 1024;
|
|
19
19
|
const MAX_COMPOSED_TEXT_LENGTH = 10_000;
|
|
20
|
+
const MAX_BUILD_REVIEW_CONTEXT_FILE_BYTES = 64 * 1024;
|
|
21
|
+
const MAX_BUILD_REVIEW_UNDERSTANDING_LENGTH = 12_000;
|
|
20
22
|
const MAX_NOTABLE_NOTE_LENGTH = 2_000;
|
|
21
23
|
const MAX_IDENTITY_INSPECTION_REASON_LENGTH = 500;
|
|
22
24
|
const MAX_ESCALATION_DECISION_NOTE_LENGTH = 2_000;
|
|
@@ -54,6 +56,62 @@ function readComposedTextFile(filePath) {
|
|
|
54
56
|
return normalized;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
function readBuildReviewContextFile(filePath) {
|
|
60
|
+
const normalizedPath = String(filePath || "").trim();
|
|
61
|
+
if (!normalizedPath) {
|
|
62
|
+
throw cliValidationError(
|
|
63
|
+
"Pass the private reviewed understanding with --review-context <context.json>.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
let contents;
|
|
67
|
+
try {
|
|
68
|
+
contents = readFileSync(normalizedPath, "utf8");
|
|
69
|
+
} catch {
|
|
70
|
+
throw cliValidationError(`Could not read ${normalizedPath}.`);
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
Buffer.byteLength(contents, "utf8") > MAX_BUILD_REVIEW_CONTEXT_FILE_BYTES
|
|
74
|
+
) {
|
|
75
|
+
throw cliValidationError(
|
|
76
|
+
"The Build review context file must be under 64KB.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(contents);
|
|
82
|
+
} catch {
|
|
83
|
+
throw cliValidationError(`${normalizedPath} is not valid JSON.`);
|
|
84
|
+
}
|
|
85
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
86
|
+
throw cliValidationError(
|
|
87
|
+
"The Build review context must be a JSON object with an understanding string.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const unexpectedKeys = Object.keys(parsed).filter(
|
|
91
|
+
(key) => key !== "understanding",
|
|
92
|
+
);
|
|
93
|
+
if (unexpectedKeys.length > 0) {
|
|
94
|
+
throw cliValidationError(
|
|
95
|
+
"The Build review context JSON may contain only the understanding field; version and provenance are server-owned.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const understanding =
|
|
99
|
+
typeof parsed.understanding === "string"
|
|
100
|
+
? parsed.understanding.trim()
|
|
101
|
+
: "";
|
|
102
|
+
if (!understanding) {
|
|
103
|
+
throw cliValidationError(
|
|
104
|
+
"The Build review context understanding must be a non-empty string.",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (understanding.length > MAX_BUILD_REVIEW_UNDERSTANDING_LENGTH) {
|
|
108
|
+
throw cliValidationError(
|
|
109
|
+
`The Build review context understanding must be at most ${MAX_BUILD_REVIEW_UNDERSTANDING_LENGTH} characters.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return understanding;
|
|
113
|
+
}
|
|
114
|
+
|
|
57
115
|
function readEditorialFile(filePath) {
|
|
58
116
|
const normalizedPath = String(filePath || "").trim();
|
|
59
117
|
if (!normalizedPath) {
|
|
@@ -243,6 +301,8 @@ export async function adminCommand(options) {
|
|
|
243
301
|
assertComposedCommentDraftResult({
|
|
244
302
|
result,
|
|
245
303
|
expectedContent: operation.body.content,
|
|
304
|
+
requiresBuildReviewContext:
|
|
305
|
+
typeof operation.body.buildReviewUnderstanding === "string",
|
|
246
306
|
});
|
|
247
307
|
}
|
|
248
308
|
if (operation.name === "daily-run.start") {
|
|
@@ -363,7 +423,11 @@ export function normalizeAdminBuildCandidatesResult({ result, siteUrl }) {
|
|
|
363
423
|
};
|
|
364
424
|
}
|
|
365
425
|
|
|
366
|
-
export function assertComposedCommentDraftResult({
|
|
426
|
+
export function assertComposedCommentDraftResult({
|
|
427
|
+
result,
|
|
428
|
+
expectedContent,
|
|
429
|
+
requiresBuildReviewContext = false,
|
|
430
|
+
}) {
|
|
367
431
|
const draft = result?.data?.draft;
|
|
368
432
|
if (
|
|
369
433
|
draft?.decision === "draft" &&
|
|
@@ -371,6 +435,25 @@ export function assertComposedCommentDraftResult({ result, expectedContent }) {
|
|
|
371
435
|
draft?.content === expectedContent &&
|
|
372
436
|
draft?.status === "ready"
|
|
373
437
|
) {
|
|
438
|
+
if (
|
|
439
|
+
requiresBuildReviewContext &&
|
|
440
|
+
draft?.buildReviewContextStored !== true
|
|
441
|
+
) {
|
|
442
|
+
const error = new Error(
|
|
443
|
+
"The API did not confirm that it stored the private Build review context. Stop without publishing this draft and deploy the context-aware API first, then retry with a new idempotency key.",
|
|
444
|
+
);
|
|
445
|
+
error.code = "LUMINE_ADMIN_BUILD_REVIEW_CONTEXT_UNSUPPORTED";
|
|
446
|
+
error.data = {
|
|
447
|
+
ok: false,
|
|
448
|
+
status: "validation_error",
|
|
449
|
+
error: {
|
|
450
|
+
code: error.code,
|
|
451
|
+
message: error.message,
|
|
452
|
+
details: null,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
374
457
|
return;
|
|
375
458
|
}
|
|
376
459
|
const error = new Error(
|
|
@@ -1255,6 +1338,9 @@ export function parseAdminOperation(options) {
|
|
|
1255
1338
|
const reviewReceipt = options.adminReviewReceipt
|
|
1256
1339
|
? parseBuildReviewReceipt(options.adminReviewReceipt)
|
|
1257
1340
|
: null;
|
|
1341
|
+
const buildReviewUnderstanding = options.adminReviewContext
|
|
1342
|
+
? readBuildReviewContextFile(options.adminReviewContext)
|
|
1343
|
+
: undefined;
|
|
1258
1344
|
if (reviewReceipt && (reviewedBuildVersionId || buildReviewMethod)) {
|
|
1259
1345
|
throw cliValidationError(
|
|
1260
1346
|
"Pass either --review-receipt or manual --reviewed-version/--reviewed-via evidence, not both.",
|
|
@@ -1266,6 +1352,22 @@ export function parseAdminOperation(options) {
|
|
|
1266
1352
|
const confirmedBuildReviewMethod = reviewReceipt
|
|
1267
1353
|
? "runtime"
|
|
1268
1354
|
: buildReviewMethod;
|
|
1355
|
+
if (
|
|
1356
|
+
buildReviewUnderstanding &&
|
|
1357
|
+
(!confirmedBuildVersionId || !confirmedBuildReviewMethod)
|
|
1358
|
+
) {
|
|
1359
|
+
throw cliValidationError(
|
|
1360
|
+
"--review-context requires confirmed Build review evidence.",
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
if (
|
|
1364
|
+
(confirmedBuildVersionId || confirmedBuildReviewMethod) &&
|
|
1365
|
+
!buildReviewUnderstanding
|
|
1366
|
+
) {
|
|
1367
|
+
throw cliValidationError(
|
|
1368
|
+
"Build review evidence requires --review-context <context.json>.",
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1269
1371
|
if (
|
|
1270
1372
|
reviewReceipt &&
|
|
1271
1373
|
parsedTarget.type === "build" &&
|
|
@@ -1281,9 +1383,13 @@ export function parseAdminOperation(options) {
|
|
|
1281
1383
|
"Build comments are management-agent composed only; pass --file <comment.md> after reviewing the project.",
|
|
1282
1384
|
);
|
|
1283
1385
|
}
|
|
1284
|
-
if (
|
|
1386
|
+
if (
|
|
1387
|
+
!confirmedBuildVersionId ||
|
|
1388
|
+
!confirmedBuildReviewMethod ||
|
|
1389
|
+
!buildReviewUnderstanding
|
|
1390
|
+
) {
|
|
1285
1391
|
throw cliValidationError(
|
|
1286
|
-
"After reviewing the project, pass
|
|
1392
|
+
"After reviewing the project, pass review evidence and --review-context <context.json>.",
|
|
1287
1393
|
);
|
|
1288
1394
|
}
|
|
1289
1395
|
} else if (
|
|
@@ -1313,6 +1419,9 @@ export function parseAdminOperation(options) {
|
|
|
1313
1419
|
...(confirmedBuildReviewMethod
|
|
1314
1420
|
? { buildReviewMethod: confirmedBuildReviewMethod }
|
|
1315
1421
|
: {}),
|
|
1422
|
+
...(buildReviewUnderstanding
|
|
1423
|
+
? { buildReviewUnderstanding }
|
|
1424
|
+
: {}),
|
|
1316
1425
|
},
|
|
1317
1426
|
);
|
|
1318
1427
|
}
|
package/lib/commands.js
CHANGED
|
@@ -2363,6 +2363,7 @@ export function parseArgs(args) {
|
|
|
2363
2363
|
adminScaffoldFile: raw.scaffold ? String(raw.scaffold) : "",
|
|
2364
2364
|
adminTargetFile: raw.targetFile ? String(raw.targetFile) : "",
|
|
2365
2365
|
adminReviewReceipt: raw.reviewReceipt ? String(raw.reviewReceipt) : "",
|
|
2366
|
+
adminReviewContext: raw.reviewContext ? String(raw.reviewContext) : "",
|
|
2366
2367
|
adminSeverity: raw.severity ? String(raw.severity) : "",
|
|
2367
2368
|
adminStatus: raw.status ? String(raw.status) : "",
|
|
2368
2369
|
adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
|
|
@@ -2766,8 +2767,8 @@ export function printHelp() {
|
|
|
2766
2767
|
lumine admin post skip <target> [--type comment|aiStory|dailyReflection] [--reason <text>] [--json]
|
|
2767
2768
|
lumine admin post skip-batch --target-file <json-or-lines> [--reason <text>] [--checkpoint <file> [--resume]] [--json]
|
|
2768
2769
|
lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
|
|
2769
|
-
lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
|
|
2770
|
-
lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
|
|
2770
|
+
lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
|
|
2771
|
+
lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
|
|
2771
2772
|
lumine admin comment post --draft-id <id> [--json]
|
|
2772
2773
|
lumine admin comment edit <comment-id> --file <comment.md> [--json]
|
|
2773
2774
|
lumine admin brief [--days <1..30>] [--json]
|
|
@@ -2864,6 +2865,7 @@ Options:
|
|
|
2864
2865
|
--scaffold <file> Write an editable newspaper editorial scaffold
|
|
2865
2866
|
--target-file <file> JSON array or newline list for audited batch skips
|
|
2866
2867
|
--review-receipt <f> Confirmed managed Build runtime review receipt
|
|
2868
|
+
--review-context <f> Private JSON understanding from the reviewed Build
|
|
2867
2869
|
--severity <level> Run escalation severity: attention or urgent
|
|
2868
2870
|
--status <state> Private escalation or todo lifecycle filter/state
|
|
2869
2871
|
--wait-ms <ms> Managed Build runtime observation time (1000-45000)
|
package/lib/constants.js
CHANGED
|
@@ -115,6 +115,8 @@ export const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
|
115
115
|
import.meta.url,
|
|
116
116
|
);
|
|
117
117
|
export const PACKAGE_METADATA_URL = new URL("../package.json", import.meta.url);
|
|
118
|
+
export const LUMINE_WORLD_UPDATE_GUIDANCE = `- For Twinkle.world realtime presence, keep render/input loops local. Queue an update only when relevant state changes, replace any queued snapshot with the newest one, and flush on a fixed 5-15 updates-per-second schedule with at most one updatePresence request in flight. Never call or await updatePresence every animation frame, resend unchanged snapshots, overlap requests, or build a backlog.
|
|
119
|
+
- Send Twinkle.world actions only when the discrete action happens; do not poll or automatically retry them. On WORLD_EVENT_RATE_LIMITED or another recoverable non-session-ended error, drop that attempted presence/action update without an immediate retry and keep the session. Reconnect with backoff only after session.ended or Twinkle.world.isSessionEndedError(error).`;
|
|
118
120
|
export const SDK_REFERENCE_FALLBACK = `${LUMINE_SDK_REFERENCE_MARKER}
|
|
119
121
|
# Twinkle Build SDK Reference
|
|
120
122
|
|
|
@@ -131,6 +133,7 @@ Use these current source-of-truth rules:
|
|
|
131
133
|
- Use Twinkle.aiStories.list/search/get for existing AI Story passage text, story media, and questions.
|
|
132
134
|
- Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }. Live web search is enabled by default; pass webSearch: false to disable it for the app.
|
|
133
135
|
- Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
|
|
136
|
+
${LUMINE_WORLD_UPDATE_GUIDANCE}
|
|
134
137
|
- Prefer existing documented Twinkle.* methods over guessing names from old code.
|
|
135
138
|
`;
|
|
136
139
|
export const LUMINE_THREE_VENDOR_GUIDANCE = `- For Three.js, use the first-party core module: import * as THREE from '${BUILD_VENDOR_THREE_MODULE_IMPORT}';
|
|
@@ -285,6 +288,7 @@ lumine save --summary "Describe the change"
|
|
|
285
288
|
${LUMINE_THREE_VENDOR_GUIDANCE}
|
|
286
289
|
- Do not invent or guess Twinkle.* SDK method names. Use ${SDK_REFERENCE_FILE} as the local SDK reference and prefer Twinkle.capabilities checks for gated features.
|
|
287
290
|
- Match storage to update frequency. Twinkle.privateDb and Twinkle.sharedDb are for LOW-frequency durable state only — things that change on a user action (settings, inventory checkpoints, completed quests, saved progress; comments, votes, room settings, submitted records). NEVER write high-frequency or per-frame/per-tick state to them (camera or cursor position, animation state, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and for durable per-user state flush an occasional snapshot on an interval or on exit (never per frame) — e.g. the viewer/user DB or a single latest-snapshot key. The server rate-limits these writes per key and returns 429 on excess; never retry-loop a 429.
|
|
291
|
+
${LUMINE_WORLD_UPDATE_GUIDANCE}
|
|
288
292
|
|
|
289
293
|
## Local Testing (Playwright / browser probes)
|
|
290
294
|
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
4
|
-
Updated: 2026-08-
|
|
5
|
-
Generated: 2026-08-
|
|
3
|
+
Version: 1.37.2
|
|
4
|
+
Updated: 2026-08-26
|
|
5
|
+
Generated: 2026-08-26T15:17:11.027Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -341,25 +341,26 @@ renderBattery(policy?.energyPercent, policy?.energySegmentsRemaining);
|
|
|
341
341
|
- Use this for in-app AI replies instead of creating or fetching app-local endpoints such as /api/chat.
|
|
342
342
|
- Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
|
|
343
343
|
const result = await Twinkle.ai.chat({ message, history: chatHistory, systemPrompt: 'You are a cheerful pirate helper who answers in one sentence.', onText: (text, meta) => renderReply(text), onStatus: (status) => setThinking(status === 'thinking') });
|
|
344
|
-
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, model, instructions, systemPrompt, webSearch, requestId, onText, onStatus } = {}) | scopes: none
|
|
344
|
+
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, model, instructions, systemPrompt, webSearch, requestId, onText, onStatus, onReasoning } = {}) | scopes: none
|
|
345
345
|
- Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, requestedModel, webSearch, aiUsagePolicy }
|
|
346
346
|
- Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic, with optional live output/status callbacks and web search.
|
|
347
347
|
- Signed-in viewers only.
|
|
348
348
|
- Use this instead of asking Twinkle.ai.chat to return JSON.
|
|
349
349
|
- expectedStructure must be a JSON object that describes the exact returned object shape.
|
|
350
350
|
- mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
|
|
351
|
-
- Omit model to use the normal Lite/Medium/High routing. model accepts
|
|
351
|
+
- Omit model to use the normal Lite/Medium/High routing. model accepts gpt-5.6-sol, claude-opus-5, or claude-fable-5, and every explicit model must be paired with thinkingMode: 'high'; unknown model IDs reject instead of silently falling back.
|
|
352
352
|
- thinkingMode low uses GPT-5.6 Luna and consumes the viewer's AI Energy from confirmed provider usage; its smaller model is usually cheaper than Medium or High.
|
|
353
353
|
- thinkingMode medium uses Grok 4.6 with medium reasoning and consumes normal AI Energy.
|
|
354
|
-
- thinkingMode high uses GPT-5.6 Sol with high reasoning and consumes high AI Energy.
|
|
354
|
+
- thinkingMode high without model uses GPT-5.6 Sol with high reasoning and consumes high AI Energy. Explicit model: 'gpt-5.6-sol' selects Sol with xhigh reasoning at the same High AI Energy tier.
|
|
355
355
|
- claude-opus-5 uses Anthropic adaptive High thinking. claude-fable-5 uses Anthropic xhigh thinking and normally consumes more AI Energy for comparable token use. Both debit confirmed provider usage at the High tier.
|
|
356
|
-
- Pass onStatus and/or onText to stream progress from the same structured generation. onStatus receives high-level phases such as thinking, searching_web, responding, validating, and completed.
|
|
356
|
+
- Pass onStatus, onReasoning, and/or onText to stream progress from the same structured generation. onStatus receives high-level phases such as thinking, searching_web, responding, validating, and completed.
|
|
357
|
+
- onReasoning receives accumulated provider-supplied, app-visible reasoning summaries plus { done, delta, requestId, status }. A provider retry may replace the accumulated summary; treat each callback's first argument as the current source of truth. This callback never exposes hidden/private model chain-of-thought.
|
|
357
358
|
- onText receives accumulated structured-output text plus { done, delta, requestId, status }. Partial output is intentionally incomplete and may include provider formatting; parse only when done is true, when the callback receives the canonical object serialized as JSON, and use the resolved object as the source of truth.
|
|
358
|
-
-
|
|
359
|
+
- Put a user-facing field such as producerNotes in expectedStructure when commentary must be part of the validated final object rather than transient reasoning progress.
|
|
359
360
|
- When AI Energy is empty, every automatic or named model choice rejects before new provider work; there is no free fallback mode.
|
|
360
361
|
- Live web search is enabled by default in Medium and High modes. Pass webSearch: false to disable it for the app. Low/Lite Mode remains tool-free; explicitly forcing webSearch: true in Low Mode returns an error.
|
|
361
|
-
- The server validates the final shape; automatic OpenAI/xAI routes can retry malformed output, while explicit Anthropic routes use native JSON Schema output. App code should still validate business-specific enum values.
|
|
362
|
-
- Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: 'high', model: 'claude-opus-5', prompt: 'Plan the next section from: ' + currentState, expectedStructure: { producerNotes: 'string', action: 'string', confidence: 0 }, onStatus: (phase) => showPhase(phase), onText: (partialJson, meta) => showStructuredProgress(partialJson, meta) });
|
|
362
|
+
- The server validates the final shape; automatic OpenAI/xAI routes can retry malformed output, while explicit Anthropic routes use native JSON Schema output and retry one malformed or shape-invalid result. App code should still validate business-specific enum values.
|
|
363
|
+
- Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: 'high', model: 'claude-opus-5', prompt: 'Plan the next section from: ' + currentState, expectedStructure: { producerNotes: 'string', action: 'string', confidence: 0 }, onStatus: (phase) => showPhase(phase), onReasoning: (summary, meta) => showReasoningProgress(summary, meta), onText: (partialJson, meta) => showStructuredProgress(partialJson, meta) });
|
|
363
364
|
- onChatStatus(listener) | scopes: none
|
|
364
365
|
- Returns: unsubscribe function
|
|
365
366
|
- Listen to shared runtime AI chat stream events.
|
|
@@ -742,7 +743,8 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
742
743
|
- Signed-in player identity comes from the canonical Twinkle user record; player.profilePicUrl is only used for guests and is returned only when it is a valid absolute HTTPS URL.
|
|
743
744
|
- Subscribe to session.ended and catch updatePresence/send errors. Stop using stale handles and reconnect only when Twinkle.world.isSessionEndedError(error) is true; for other Twinkle.world.isRecoverableSessionError(error) cases, drop the transient presence/action and keep the handle.
|
|
744
745
|
- Use updatePresence for live avatar snapshots and send for lightweight actions such as emotes, interactions, and chat bubbles.
|
|
745
|
-
-
|
|
746
|
+
- Treat the render/input loop as local-only. Queue presence only after relevant fields change, replace any queued snapshot with the newest one, and flush on a fixed 5-15 updates-per-second schedule with at most one updatePresence request in flight. Never call or await updatePresence every animation frame, resend unchanged snapshots, overlap requests, or build a backlog.
|
|
747
|
+
- Send discrete actions only when they happen; do not poll or automatically retry them. The parent limits updatePresence and send together to protect the website connection. WORLD_EVENT_RATE_LIMITED is recoverable: drop that attempted update without an immediate retry and keep the current session.
|
|
746
748
|
- Rooms are addressed by worldKey, roomKey, and instanceId so the contract can later move to sharded or dedicated game backends.
|
|
747
749
|
- Example: const world = await Twinkle.world.join({ roomKey: 'town-square', presence: { x: 0, y: 0, z: 0, facing: 'south' }, player: { name: avatarName } });
|
|
748
750
|
world.subscribe((event) => updateRemotePlayers(event.players));
|
|
@@ -750,8 +752,8 @@ world.updatePresence({ x, y, z, facing });
|
|
|
750
752
|
- isRecoverableSessionError(error) | scopes: none
|
|
751
753
|
- Returns: boolean
|
|
752
754
|
- Return true when a world request error is expected to be handled by app code instead of crashing.
|
|
753
|
-
- Recoverable session errors include ended, missing, socket-disconnected, socket-not-ready, room-missing, preview-updating, and timed-out world session requests.
|
|
754
|
-
- Only session-ended errors prove that the current handle should be discarded.
|
|
755
|
+
- Recoverable session errors include ended, missing, socket-disconnected, socket-not-ready, room-missing, rate-limited, preview-updating, and timed-out world session requests.
|
|
756
|
+
- Only session-ended errors prove that the current handle should be discarded. WORLD_EVENT_RATE_LIMITED, timed-out, or preview-updating presence/action requests must be dropped without reconnecting and without an immediate retry.
|
|
755
757
|
- For durable game state, write through sharedDb/privateDb instead of relying on world presence — but LOW-frequency only (on a user action or an occasional snapshot, never per frame/tick).
|
|
756
758
|
- Example: try {
|
|
757
759
|
await world.updatePresence({ x, y, z, facing });
|
|
@@ -1028,59 +1030,104 @@ Keywords: multiplayer, mmo, town, presence, avatars, movement, three.js, realtim
|
|
|
1028
1030
|
|
|
1029
1031
|
```js
|
|
1030
1032
|
let world = null;
|
|
1033
|
+
let worldConnection = null;
|
|
1031
1034
|
let reconnectTimer = 0;
|
|
1035
|
+
let reconnectDelayMs = 1000;
|
|
1036
|
+
let latestPresence = { x: 0, y: 0, z: 0, facing: 'south', animation: 'idle' };
|
|
1037
|
+
let latestPresenceKey = JSON.stringify(latestPresence);
|
|
1038
|
+
let queuedPresence = null;
|
|
1039
|
+
let presenceInFlight = false;
|
|
1032
1040
|
|
|
1033
1041
|
async function connectWorld() {
|
|
1034
1042
|
if (world) return world;
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1043
|
+
if (worldConnection) return worldConnection;
|
|
1044
|
+
const presenceAtJoin = latestPresence;
|
|
1045
|
+
const presenceKeyAtJoin = latestPresenceKey;
|
|
1046
|
+
worldConnection = Twinkle.world.join({
|
|
1047
|
+
worldKey: 'town', roomKey: 'square', presence: presenceAtJoin,
|
|
1039
1048
|
player: { name: avatarName }
|
|
1040
1049
|
});
|
|
1050
|
+
try {
|
|
1051
|
+
const session = await worldConnection;
|
|
1052
|
+
world = session;
|
|
1053
|
+
reconnectDelayMs = 1000;
|
|
1054
|
+
session.subscribe((event) => {
|
|
1055
|
+
renderPlayers(event.players);
|
|
1056
|
+
if (event.type === 'session.ended') handleWorldDrop(session);
|
|
1057
|
+
if (event.type === 'action.received' && event.action?.type === 'emote') {
|
|
1058
|
+
showEmote(event.sessionId, event.action.data.emote);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
if (latestPresenceKey !== presenceKeyAtJoin) queuedPresence = latestPresence;
|
|
1062
|
+
return session;
|
|
1063
|
+
} finally {
|
|
1064
|
+
worldConnection = null;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1041
1067
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1068
|
+
function handleWorldConnectError(error) {
|
|
1069
|
+
if (Twinkle.world.isRecoverableSessionError(error)) {
|
|
1070
|
+
scheduleReconnect();
|
|
1071
|
+
} else {
|
|
1072
|
+
console.error('World connection failed', error);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function scheduleReconnect() {
|
|
1077
|
+
if (reconnectTimer || world || worldConnection) return;
|
|
1078
|
+
const delay = reconnectDelayMs;
|
|
1079
|
+
reconnectDelayMs = Math.min(30000, reconnectDelayMs * 2);
|
|
1080
|
+
reconnectTimer = setTimeout(() => {
|
|
1081
|
+
reconnectTimer = 0;
|
|
1082
|
+
connectWorld().catch(handleWorldConnectError);
|
|
1083
|
+
}, delay);
|
|
1052
1084
|
}
|
|
1053
1085
|
|
|
1054
|
-
function handleWorldDrop() {
|
|
1086
|
+
function handleWorldDrop(session = world) {
|
|
1087
|
+
if (session && world && world !== session) return;
|
|
1055
1088
|
world = null;
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1089
|
+
queuedPresence = null;
|
|
1090
|
+
scheduleReconnect();
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function queuePresence(next) {
|
|
1094
|
+
const key = JSON.stringify(next);
|
|
1095
|
+
if (key === latestPresenceKey) return;
|
|
1096
|
+
latestPresenceKey = key;
|
|
1097
|
+
latestPresence = next;
|
|
1098
|
+
if (world) queuedPresence = next; // Coalesce to the newest unsent snapshot.
|
|
1062
1099
|
}
|
|
1063
1100
|
|
|
1064
|
-
async function
|
|
1101
|
+
async function flushPresence() {
|
|
1102
|
+
if (presenceInFlight || !queuedPresence || !world) return;
|
|
1103
|
+
const session = world;
|
|
1104
|
+
const next = queuedPresence;
|
|
1105
|
+
queuedPresence = null;
|
|
1106
|
+
presenceInFlight = true;
|
|
1065
1107
|
try {
|
|
1066
|
-
|
|
1067
|
-
// Throttle this in the game loop, for example 5-15 times per second.
|
|
1068
|
-
await session.updatePresence({ x: player.x, y: player.y, z: player.z, facing });
|
|
1108
|
+
await session.updatePresence(next);
|
|
1069
1109
|
} catch (error) {
|
|
1070
1110
|
if (Twinkle.world.isSessionEndedError(error)) {
|
|
1071
|
-
handleWorldDrop();
|
|
1072
|
-
|
|
1111
|
+
handleWorldDrop(session);
|
|
1112
|
+
} else if (!Twinkle.world.isRecoverableSessionError(error)) {
|
|
1113
|
+
console.error('World update failed', error);
|
|
1073
1114
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1078
|
-
throw error;
|
|
1115
|
+
// Recoverable errors drop this transient snapshot without an immediate retry.
|
|
1116
|
+
} finally {
|
|
1117
|
+
presenceInFlight = false;
|
|
1079
1118
|
}
|
|
1080
1119
|
}
|
|
1081
1120
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1121
|
+
// The render/input loop only queues changed local state.
|
|
1122
|
+
function onPlayerStateChanged() {
|
|
1123
|
+
queuePresence({
|
|
1124
|
+
x: player.x, y: player.y, z: player.z,
|
|
1125
|
+
facing, animation: player.animation
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
connectWorld().catch(handleWorldConnectError);
|
|
1130
|
+
setInterval(() => { void flushPresence(); }, 100); // Fixed 10 Hz cap.
|
|
1084
1131
|
```
|
|
1085
1132
|
|
|
1086
1133
|
### Play chess against the computer
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -34,8 +34,10 @@ canonical structured data.
|
|
|
34
34
|
ordering key for the People directory, so the bots now surface there after a
|
|
35
35
|
run; that is the intended consequence of the timestamp being truthful.
|
|
36
36
|
- A later human reply to a delegated Zero/Ciel comment enters the existing
|
|
37
|
-
server-side autonomous comment-assistant pipeline.
|
|
38
|
-
|
|
37
|
+
server-side autonomous comment-assistant pipeline. For Build threads this is
|
|
38
|
+
limited to comments carrying the private reviewed-management provenance
|
|
39
|
+
described below. The human's normal AI Energy and sponsor path applies;
|
|
40
|
+
Lumine does not need to remain running.
|
|
39
41
|
|
|
40
42
|
`auto` selects Zero first when no completed rotation exists, then alternates
|
|
41
43
|
after a successfully completed run that performed a mutation. Failed,
|
|
@@ -887,8 +889,11 @@ captures a screenshot and bounded console evidence, then fetches the identity
|
|
|
887
889
|
again. It writes `review.json` in a unique per-review subdirectory only when
|
|
888
890
|
the browser completed, the screenshot exists, and the artifact did not change
|
|
889
891
|
mid-review. Attach the returned `receiptPath` with
|
|
890
|
-
`comment draft ... --review-receipt review.json
|
|
891
|
-
|
|
892
|
+
`comment draft ... --review-receipt review.json`, and pass a separate private
|
|
893
|
+
`--review-context context.json` containing only the concrete `understanding`
|
|
894
|
+
learned during that review. The receipt binds the draft to the exact reviewed
|
|
895
|
+
artifact without copying a version number by hand; the server owns the Build,
|
|
896
|
+
version, method, and review-time fields around that understanding.
|
|
892
897
|
|
|
893
898
|
During every management run, scan recent Build candidates back through the
|
|
894
899
|
run's review window alongside Subjects and the recommendation queue. An app
|
|
@@ -2315,10 +2320,17 @@ to the exact published artifact you saw:
|
|
|
2315
2320
|
|
|
2316
2321
|
```bash
|
|
2317
2322
|
lumine admin comment draft build:884 --file comment.md \
|
|
2318
|
-
--reviewed-version 4512 --reviewed-via runtime
|
|
2323
|
+
--reviewed-version 4512 --reviewed-via runtime \
|
|
2324
|
+
--review-context context.json --json
|
|
2319
2325
|
lumine admin comment post --draft-id 77 --json
|
|
2320
2326
|
```
|
|
2321
2327
|
|
|
2328
|
+
```json
|
|
2329
|
+
{
|
|
2330
|
+
"understanding": "The start screen pairs a green zombie with a sci-fi soldier, and the first interaction teaches the player to select a zombie before firing the railgun."
|
|
2331
|
+
}
|
|
2332
|
+
```
|
|
2333
|
+
|
|
2322
2334
|
Use `--reviewed-via code` only when you actually pulled and read the project;
|
|
2323
2335
|
`runtime` means you opened and tried the published app. Replies to human
|
|
2324
2336
|
comments inside a Build use `comment:<id>` plus the same review flags. The API
|
|
@@ -2327,9 +2339,20 @@ published version, a private/noncanonical Build, or any Build/comment-context
|
|
|
2327
2339
|
change between draft and publication. A changed version means review the new
|
|
2328
2340
|
project state and compose again. The flags are an auditable statement of what
|
|
2329
2341
|
the management agent did, not permission to infer experience from metadata.
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2342
|
+
The review-context JSON is private server-owned conversation provenance; it is
|
|
2343
|
+
not included in the public comment, draft response, socket payload, or audit
|
|
2344
|
+
metadata. A direct human reply to the resulting Zero/Ciel management comment,
|
|
2345
|
+
or to a later reply by that same bot descended from it, may enter the ordinary
|
|
2346
|
+
comment-assistant pipeline using this stored historical understanding. It uses
|
|
2347
|
+
the human commenter's normal AI Energy and reply-or-Like gate. If that user has
|
|
2348
|
+
no remaining AI Energy, the normal sponsor placeholder/button is shown and a
|
|
2349
|
+
sponsor pays from their own battery. Mentions elsewhere in a Build, replies to
|
|
2350
|
+
unlinked or legacy bot comments, replies to humans, and the other bot remain
|
|
2351
|
+
ineligible. If the published version has changed, the responder is told the
|
|
2352
|
+
stored understanding belongs to the reviewed older version and must say it has
|
|
2353
|
+
not checked behavior that could have changed. The generic `comment edit`
|
|
2354
|
+
shortcut is also disabled there; review the current version and post a
|
|
2355
|
+
version-bound correction reply instead.
|
|
2333
2356
|
|
|
2334
2357
|
**Offer a Lumine prompt when the moment invites it (Mikey's direction,
|
|
2335
2358
|
2026-08-10).** Zero and Ciel may include one concrete, copy-pasteable Lumine
|
|
@@ -2362,6 +2385,11 @@ silently ignores `content` and generates with the server's model instead. The
|
|
|
2362
2385
|
CLI therefore requires the ready draft response to echo the exact submitted
|
|
2363
2386
|
text with `reason: "operator-composed"`; otherwise it stops with
|
|
2364
2387
|
`LUMINE_ADMIN_COMPOSED_COMMENT_UNSUPPORTED`. Never publish that rejected draft.
|
|
2388
|
+
For Build drafts it additionally requires
|
|
2389
|
+
`buildReviewContextStored: true`; an older API that ignores the private context
|
|
2390
|
+
fails closed with `LUMINE_ADMIN_BUILD_REVIEW_CONTEXT_UNSUPPORTED`. Deploy the
|
|
2391
|
+
context-aware API, then retry with a new idempotency key rather than publishing
|
|
2392
|
+
the unconfirmed draft.
|
|
2365
2393
|
Placement stays on the requested target: compose replies via `comment:<id>`
|
|
2366
2394
|
targets (the generated path's model-chosen
|
|
2367
2395
|
`replyTargetCommentId` does not apply). Everything below about targets,
|
|
@@ -2379,14 +2407,17 @@ A draft targets one of:
|
|
|
2379
2407
|
|
|
2380
2408
|
A reply's container resolves canonically from the target comment: its subject,
|
|
2381
2409
|
Build, or AI Story / Daily Reflection root. Comments under any other root are
|
|
2382
|
-
rejected with `CLI_ADMIN_UNSUPPORTED_REPLY_ROOT`. Build replies
|
|
2383
|
-
same exact-version review evidence
|
|
2410
|
+
rejected with `CLI_ADMIN_UNSUPPORTED_REPLY_ROOT`. Build replies authored by the
|
|
2411
|
+
management agent require the same exact-version review evidence and private
|
|
2412
|
+
review context as top-level Build comments. Replies to
|
|
2384
2413
|
Zero/Ciel comments and to notification comments are rejected with
|
|
2385
2414
|
`CLI_ADMIN_INVALID_REPLY_TARGET` — the bots never thread with themselves or
|
|
2386
|
-
each other
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2415
|
+
each other through the delegated CLI. A human's later direct reply to a
|
|
2416
|
+
context-backed Build management comment may enter the energy-gated autonomous
|
|
2417
|
+
pipeline described above; ordinary human replies elsewhere follow the existing
|
|
2418
|
+
comment-assistant rules. Published replies carry the ordinary thread linkage
|
|
2419
|
+
(thread root and reply-to), and notification fan-out uses the normal canonical
|
|
2420
|
+
path.
|
|
2390
2421
|
|
|
2391
2422
|
```ts
|
|
2392
2423
|
type CommentDraft = Success<{
|
|
@@ -2402,6 +2433,7 @@ type CommentDraft = Success<{
|
|
|
2402
2433
|
commentMode: "draft" | "post";
|
|
2403
2434
|
personaRevision: string; // SHA-256; raw prompt is never returned
|
|
2404
2435
|
contextRevision: string; // SHA-256 of canonical container/comments/target
|
|
2436
|
+
buildReviewContextStored: boolean; // true before a Build draft may publish
|
|
2405
2437
|
decision: "draft" | "skip";
|
|
2406
2438
|
reason: string | null;
|
|
2407
2439
|
content: string | null;
|