@stage5/lumine 0.2.79 → 0.2.81
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/admin.js +35 -4
- package/lib/rewards.js +1 -1
- package/lib/sdk.js +10 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +14 -3
- package/sdk/LUMINE_ADMIN.md +128 -26
package/lib/admin.js
CHANGED
|
@@ -221,7 +221,7 @@ export function readRewardConfigFile(filePath) {
|
|
|
221
221
|
const normalizedPath = String(filePath || "").trim();
|
|
222
222
|
if (!normalizedPath) {
|
|
223
223
|
throw cliValidationError(
|
|
224
|
-
"Pass the earning rules with --config <rules.json> (
|
|
224
|
+
"Pass the earning rules with --config <rules.json> (userDailyXP, userDailyCoins, optional userDailyClaims, rules[]).",
|
|
225
225
|
);
|
|
226
226
|
}
|
|
227
227
|
let contents;
|
|
@@ -3619,13 +3619,17 @@ function printAdminEnergyBudget(energyBudget) {
|
|
|
3619
3619
|
);
|
|
3620
3620
|
}
|
|
3621
3621
|
console.log(
|
|
3622
|
-
"day | charged | overflow | users | rechg | runs | calls/run avg/p90 | $/run avg/p90 | stops chg/unchg | busy | telemetry rows",
|
|
3622
|
+
"day | charged | overflow | users | rechg | runs | calls/run avg/p90 | $/run avg/p90 | stops chg/unchg | busy | queue rst/stop/resume | telemetry rows",
|
|
3623
3623
|
);
|
|
3624
3624
|
for (const day of energyBudget.byDay || []) {
|
|
3625
3625
|
const m = day.telemetry.metrics;
|
|
3626
3626
|
const runs = day.runs.total;
|
|
3627
|
+
// Queue metrics are absent from APIs older than the queued-request telemetry.
|
|
3628
|
+
const queue = ["queued_restored", "queued_stopped", "busy_resume_requested"]
|
|
3629
|
+
.map((metric) => String(m[metric]?.count ?? "-"))
|
|
3630
|
+
.join("/");
|
|
3627
3631
|
console.log(
|
|
3628
|
-
`${day.dayKey}${day.inProgress ? "*" : " "}| ${formatAdminUsd(day.chargedUsd).padStart(7)} | ${formatAdminUsd(day.overflowUsd).padStart(8)} | ${String(day.users).padStart(5)} | ${String(day.recharges).padStart(5)} | ${String(runs.runs).padStart(4)} | ${String(runs.callsPerRun.avg).padStart(7)}/${String(runs.callsPerRun.p90).padEnd(9)} | ${formatAdminUsd(runs.usdPerRun.avg).padStart(6)}/${formatAdminUsd(runs.usdPerRun.p90).padEnd(6)} | ${String(m.budget_stop_changed.count).padStart(7)}/${String(m.budget_stop_unchanged.count).padEnd(7)} | ${String(m.busy_refusal.count).padStart(4)} | ${day.telemetry.rowCount}`,
|
|
3632
|
+
`${day.dayKey}${day.inProgress ? "*" : " "}| ${formatAdminUsd(day.chargedUsd).padStart(7)} | ${formatAdminUsd(day.overflowUsd).padStart(8)} | ${String(day.users).padStart(5)} | ${String(day.recharges).padStart(5)} | ${String(runs.runs).padStart(4)} | ${String(runs.callsPerRun.avg).padStart(7)}/${String(runs.callsPerRun.p90).padEnd(9)} | ${formatAdminUsd(runs.usdPerRun.avg).padStart(6)}/${formatAdminUsd(runs.usdPerRun.p90).padEnd(6)} | ${String(m.budget_stop_changed.count).padStart(7)}/${String(m.budget_stop_unchanged.count).padEnd(7)} | ${String(m.busy_refusal.count).padStart(4)} | ${queue.padEnd(21)} | ${day.telemetry.rowCount}`,
|
|
3629
3633
|
);
|
|
3630
3634
|
}
|
|
3631
3635
|
console.log("* = in-progress UTC day; never headline it.");
|
|
@@ -3940,7 +3944,7 @@ function printRewardReviewResult({ operation, data }) {
|
|
|
3940
3944
|
const config = review.config || {};
|
|
3941
3945
|
if (Array.isArray(config.rules)) {
|
|
3942
3946
|
console.log(
|
|
3943
|
-
`Budgets:
|
|
3947
|
+
`Budgets: per user/day ${config.userDailyXP} XP/${config.userDailyCoins} Coins${config.userDailyClaims ? ` · ${config.userDailyClaims} claim(s)` : ""}`,
|
|
3944
3948
|
);
|
|
3945
3949
|
for (const rule of config.rules)
|
|
3946
3950
|
console.log(` ${formatRewardRuleLine(rule)}`);
|
|
@@ -3957,6 +3961,32 @@ function printRewardReviewResult({ operation, data }) {
|
|
|
3957
3961
|
console.log("Use --json for the full record.");
|
|
3958
3962
|
}
|
|
3959
3963
|
|
|
3964
|
+
function formatAdminCounts(counts) {
|
|
3965
|
+
return Object.entries(counts || {})
|
|
3966
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
3967
|
+
.map(([key, count]) => `${key} ${count}`)
|
|
3968
|
+
.join(" · ");
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
function printRewardReviewLifecycle(lifecycle) {
|
|
3972
|
+
// Absent from APIs older than the review-lifecycle telemetry.
|
|
3973
|
+
if (!lifecycle) return;
|
|
3974
|
+
const totals = lifecycle.totals || {};
|
|
3975
|
+
if (!Object.keys(totals.actions || {}).length) {
|
|
3976
|
+
console.log("Review lifecycle: no review events in the window.");
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
console.log(`Review lifecycle: ${formatAdminCounts(totals.actions)}.`);
|
|
3980
|
+
if (Object.keys(totals.thumbnails || {}).length)
|
|
3981
|
+
console.log(
|
|
3982
|
+
` automatic thumbnail after review publishes: ${formatAdminCounts(totals.thumbnails)}`,
|
|
3983
|
+
);
|
|
3984
|
+
if (Object.keys(totals.refusals || {}).length)
|
|
3985
|
+
console.log(
|
|
3986
|
+
` refused decisions (decision:code): ${formatAdminCounts(totals.refusals)}`,
|
|
3987
|
+
);
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3960
3990
|
function printRewardActivity(data) {
|
|
3961
3991
|
const apps = Array.isArray(data.apps) ? data.apps : [];
|
|
3962
3992
|
const suspects = Array.isArray(data.suspects) ? data.suspects : [];
|
|
@@ -3968,6 +3998,7 @@ function printRewardActivity(data) {
|
|
|
3968
3998
|
` app ${app.buildId} ${app.title}: ${app.claims} claim(s) · ${app.earners} earner(s) · ${app.xp} XP · ${app.coins} Coins · ${app.flagged} flagged`,
|
|
3969
3999
|
);
|
|
3970
4000
|
}
|
|
4001
|
+
printRewardReviewLifecycle(data.reviewLifecycle);
|
|
3971
4002
|
if (!suspects.length) {
|
|
3972
4003
|
console.log(
|
|
3973
4004
|
"Nothing unusual: no claim on the minimum time, no bursts, no sweeps, no repeated cap days, no guessing.",
|
package/lib/rewards.js
CHANGED
|
@@ -214,7 +214,7 @@ function printRewardsHelp() {
|
|
|
214
214
|
lumine rewards sheet --show Summarize the sheet on file (never prints answer keys)
|
|
215
215
|
|
|
216
216
|
rewards.json (project root) declares the economy the reviewer approves:
|
|
217
|
-
{ "
|
|
217
|
+
{ "userDailyXP", "userDailyCoins", "userDailyClaims"?,
|
|
218
218
|
"rules": [{ "id", "title", "xp", "coins", "verifier": "numeric-quiz" | "completion",
|
|
219
219
|
"maxAttempts"?, "retry"?: { "xpPercent", "coinsPercent", "paidAttempts"? }, "minSeconds"? (completion), "progression"?: "dated" | "until-earned" (quiz) }] }
|
|
220
220
|
Questions and answer keys never go in project files; they belong in the sheet.`);
|
package/lib/sdk.js
CHANGED
|
@@ -180,6 +180,16 @@ export const SDK_CLI_METHODS = {
|
|
|
180
180
|
readOnly: true,
|
|
181
181
|
mapArgs: (args) => ({ challengeId: args.challengeId }),
|
|
182
182
|
},
|
|
183
|
+
"rewards.getTimeline": {
|
|
184
|
+
path: "api/rewards/timeline", special: "rewards", operation: "timeline",
|
|
185
|
+
scopes: ["rewards:claim"], readOnly: true,
|
|
186
|
+
mapArgs: (args) => ({ ruleId: args.ruleId, cursor: args.cursor, limit: args.limit }),
|
|
187
|
+
},
|
|
188
|
+
"rewards.getArchivedProblem": {
|
|
189
|
+
path: "api/rewards/archived-problem", special: "rewards", operation: "archived-problem",
|
|
190
|
+
scopes: ["rewards:claim"], readOnly: true,
|
|
191
|
+
mapArgs: (args) => ({ receiptId: args.receiptId }),
|
|
192
|
+
},
|
|
183
193
|
"rewards.start": {
|
|
184
194
|
path: "api/rewards/start",
|
|
185
195
|
special: "rewards",
|
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.45.
|
|
3
|
+
Version: 1.45.2
|
|
4
4
|
Updated: 2026-09-14
|
|
5
|
-
Generated: 2026-09-
|
|
5
|
+
Generated: 2026-09-19T04:22:12.342Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -33,7 +33,7 @@ Generated: 2026-09-14T06:42:36.041Z
|
|
|
33
33
|
- Use Twinkle.live for one-way app livestreams and Twinkle.chat for the accompanying thread. Free livestreams require a verified host, end after at most 15 minutes, and issue at most 10 private viewer grants. Twinkle keeps platform-owned live-status/end controls above active hosts, so app code cannot hide or replace the broadcaster's Stop path.
|
|
34
34
|
- Media Energy is separate from AI Energy. Replace Media Energy UI only from canonical mediaEnergy/getUsage responses; never decrement, reserve, or synthesize it in app code.
|
|
35
35
|
- Twinkle.rewards awards real XP and Coins only in the current approved published release. Drafts, local previews, private apps and superseded releases cannot earn. The server supplies a published-runtime grant; app code cannot choose a recipient or award amount.
|
|
36
|
-
- The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (
|
|
36
|
+
- The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (userDailyXP, userDailyCoins, optional userDailyClaims; there is no app-wide daily or lifetime budget, only what one learner can earn per day) and rules [{ id, title, xp, coins, verifier: 'numeric-quiz' | 'completion', maxAttempts?, retry?: { xpPercent, coinsPercent, paidAttempts? }, minSeconds? (completion), progression?: 'dated' | 'until-earned' (quiz) }]. Wire the matching Twinkle.rewards calls with those literal rule ids. Questions and answer keys NEVER go in project files (published source is readable by every player): quiz rules get them from the private question sheet uploaded with `lumine rewards sheet <file.json>` ({ rules: { <ruleId>: { questions?, sets? } } }); `lumine rewards check` validates both together. A review request freezes the code and proposes rewards.json merged with the sheet; the administrator reads the code, checks the amounts and whether the app is exploitable, may change any amount, and approves. Creators are kids and teens: show approval status and one Send for review action; do not ask them to fill in technical forms. Every code update that retains rewards needs a new approval before publishing. Removing the SDK automatically clears its gate. Apps read amounts, tries and sets from getStatus, never from their own file.
|
|
37
37
|
- Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per site day (UTC midnight), and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified.
|
|
38
38
|
- Numeric quiz answers are verified on the server; client scores, privateDb state, timers and completion booleans are not verified reward evidence. Limits reset at UTC midnight. Rules are earned once per viewer per UTC day; attempt limits and retry payouts come from the approved rule. Challenges expire at the UTC day boundary. Budgets apply across release changes.
|
|
39
39
|
|
|
@@ -1037,6 +1037,17 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1037
1037
|
- Standings of who earned the most XP or Coins in THIS app, computed by Twinkle from its own receipts (never from anything the app submits). period 'day' is today (site day, UTC), 'week' the last 7 site days, 'all' (default) every day since approval. limit defaults to 20, max 100.
|
|
1038
1038
|
- available says which boards this app's approved rules can pay: show a Coins board only when available.coins is true (an app whose rules pay XP only has no Coins standings). me is the signed-in viewer's own standing even when they fall outside the page, or null when they earned nothing in the period.
|
|
1039
1039
|
- Drafts and previews return mode 'preview' with no entries. Use Twinkle.leaderboards for app-defined scores; use this for real XP and Coins earned.
|
|
1040
|
+
- await Twinkle.rewards.getTimeline({ ruleId?, cursor?, limit? } = {}) | scopes: rewards:claim
|
|
1041
|
+
- Returns: { mode: "live", dayKey, entries: [{ receiptId, ruleId, ruleTitle, setKey, title, promptPreview, dayKey, closedAt, solvedAt, solver: { userId, username }, firstSolver, xp, coins, attempt }], nextCursor } | { mode: "preview", entries: [], nextCursor: null, message }
|
|
1042
|
+
- Browse confirmed solves of retired until-earned quiz bounties, newest first.
|
|
1043
|
+
- A solve appears only after the next site reset (UTC midnight, 9 AM Korea). Today’s solves, unsolved sets, standing quizzes and dated quizzes are excluded. The server decides retirement; client dates cannot unlock content. Zero-reward correct solves are included.
|
|
1044
|
+
- One entry per solve receipt, with firstSolver identifying the first receipt for that rule and set across approved versions. limit defaults to 20, max 50; pass nextCursor unchanged for older solves and omit it when changing ruleId. Pages may have fewer entries when old question sheets cannot be recovered; continue while nextCursor is present.
|
|
1045
|
+
- No daily-claim limit applies to reading. Requires the current approved published runtime grant; previews return an empty timeline. Never awards or changes balances. Open a receipt with getArchivedProblem to load the original question and guide.
|
|
1046
|
+
- await Twinkle.rewards.getArchivedProblem({ receiptId }) | scopes: rewards:claim
|
|
1047
|
+
- Returns: { mode: "live", entry: <same solve entry as getTimeline>, questions: [{ prompt, hint?, guide? }] } | { mode: "preview", entry: null, questions: [], message }
|
|
1048
|
+
- Read a retired bounty’s original questions and guides from the approval attached to its solve receipt.
|
|
1049
|
+
- Use a receiptId returned by getTimeline. Both reads independently check retirement and app ownership; guessing an active or other app’s receipt cannot reveal its questions or guides. Returns build_reward_archive_unavailable (404) if unavailable.
|
|
1050
|
+
- Uses that receipt’s frozen approved sheet, never today’s edited question or the mutable draft. Answer keys and tolerances are never returned. Render the question first and offer Reveal guide for learning, without a reward-claim button.
|
|
1040
1051
|
|
|
1041
1052
|
## Examples
|
|
1042
1053
|
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1018,6 +1018,34 @@ Mikey gives his go-ahead; a pending proposal is not a completed refresh. After
|
|
|
1018
1018
|
approval, execute the entire approved plan and verify it without asking for
|
|
1019
1019
|
each swap again. Never omit this section from a full-run report.
|
|
1020
1020
|
|
|
1021
|
+
### Full management report in Chrome
|
|
1022
|
+
|
|
1023
|
+
Mikey's standing delivery preference (2026-09-15): after a full daily run, open
|
|
1024
|
+
the **complete management report as a browsable localhost page in Chrome**.
|
|
1025
|
+
Do this as part of finishing the authorized run; a Markdown path in chat alone
|
|
1026
|
+
is insufficient, and no additional confirmation is needed to open the report.
|
|
1027
|
+
|
|
1028
|
+
1. Save the complete report as
|
|
1029
|
+
`/private/tmp/twinkle-daily-YYYY-MM-DD/daily-management-report.md`, using the
|
|
1030
|
+
run's Bangkok date. Include every required reporting section, coverage gap,
|
|
1031
|
+
pending decision and carryover. Reflect later owner decisions accurately.
|
|
1032
|
+
2. Render the entire Markdown into a readable HTML page with section links,
|
|
1033
|
+
usable tables and access to the original Markdown. Preserve complete
|
|
1034
|
+
appendices and flagged rows; navigation or collapsible details must not
|
|
1035
|
+
discard them. Use local assets so the report does not depend on a CDN.
|
|
1036
|
+
3. Serve the view on `127.0.0.1` using an available port. Expose only the HTML,
|
|
1037
|
+
its required assets and the report Markdown through a dedicated directory
|
|
1038
|
+
or explicit routes; do not serve the surrounding private evidence folder.
|
|
1039
|
+
Keep the server available after the response so Mikey can browse it.
|
|
1040
|
+
4. Open the localhost URL in Mikey's Chrome. Verify that the page renders,
|
|
1041
|
+
section navigation works, and the full report is accessible. Keep the
|
|
1042
|
+
report tab open. Include both the localhost URL and Markdown file link in
|
|
1043
|
+
the final response.
|
|
1044
|
+
|
|
1045
|
+
For a follow-up that only opens or updates an existing report, reuse that
|
|
1046
|
+
report and its existing tab/server where available. This does not authorize
|
|
1047
|
+
starting another management run or repeating unrelated daily duties.
|
|
1048
|
+
|
|
1021
1049
|
Creating an escalation belongs to the active run; acknowledging, annotating,
|
|
1022
1050
|
resolving, or reopening it does not. Use the run-independent `escalation`
|
|
1023
1051
|
commands after Mikey responds instead of starting a follow-up delegated run.
|
|
@@ -1115,17 +1143,18 @@ Review questions to settle with Mikey before approving:
|
|
|
1115
1143
|
it, then the next one comes up the following site day (UTC midnight, 9:00 AM Korea)) keep them honest.
|
|
1116
1144
|
- Do the rule IDs in `rewards.json` match what the code starts? Unknown IDs
|
|
1117
1145
|
simply never pay.
|
|
1118
|
-
- Are the amounts and the per-
|
|
1119
|
-
|
|
1146
|
+
- Are the amounts and the per-learner daily budgets right for what the app
|
|
1147
|
+
actually asks of people? There is no app-wide budget, per day or lifetime:
|
|
1148
|
+
a good app keeps paying everyone who plays it. Older configs may still
|
|
1149
|
+
carry `dailyXP`/`dailyCoins`/`lifetimeXP`/`lifetimeCoins`; the API accepts
|
|
1150
|
+
and ignores them, so never add or tune them.
|
|
1120
1151
|
|
|
1121
1152
|
`rules.json` (what `--config` takes, and what the app's `rewards.json` plus
|
|
1122
1153
|
sheet compose into):
|
|
1123
1154
|
|
|
1124
1155
|
```json
|
|
1125
1156
|
{
|
|
1126
|
-
"dailyXP": 2000000, "dailyCoins": 0,
|
|
1127
1157
|
"userDailyXP": 10000, "userDailyCoins": 0,
|
|
1128
|
-
"lifetimeXP": 200000000, "lifetimeCoins": 0,
|
|
1129
1158
|
"rules": [
|
|
1130
1159
|
{ "id": "stage-1", "title": "Clear Stage 1", "xp": 300, "coins": 0,
|
|
1131
1160
|
"verifier": "completion", "minSeconds": 20 },
|
|
@@ -1163,8 +1192,8 @@ day, elementary 50,000 XP + 1,000 Coins, middle 70,000 + 5,000, high
|
|
|
1163
1192
|
`userDailyClaims` 1; until-earned sets authored from the Korean curriculum.
|
|
1164
1193
|
Arcade Typing (Mikey, 2026-09-12): XP for clearing campaign stages, up to
|
|
1165
1194
|
10,000 XP per learner per day, no Coins. Platform ceilings: 100,000 XP /
|
|
1166
|
-
10,000 Coins per rule and per learner per day
|
|
1167
|
-
per
|
|
1195
|
+
10,000 Coins per rule and per learner per day. No app-wide ceiling exists,
|
|
1196
|
+
per day or lifetime.
|
|
1168
1197
|
|
|
1169
1198
|
Approval freezes these rules with the reviewed snapshot and publishes that
|
|
1170
1199
|
snapshot immediately (the result carries `published.version`); an approval
|
|
@@ -1239,6 +1268,24 @@ open the player with `admin identity inspect` before proposing anything, and
|
|
|
1239
1268
|
propose to Mikey (revoke the app's rule, or a bucket ban) rather than acting.
|
|
1240
1269
|
Never revoke an approval from a daily run.
|
|
1241
1270
|
|
|
1271
|
+
The same report carries `reviewLifecycle` (added 2026-09-15): the reviews'
|
|
1272
|
+
audit trail counted per UTC day (`byDay`) and for the window (`totals`).
|
|
1273
|
+
`actions` counts every event: `request`, `approve`/`reject`/`revoke`,
|
|
1274
|
+
`publish` (the approved version went live), `propose`, `proposal_viewed` (the
|
|
1275
|
+
creator opened an offer's comparison; once per revision), `accept`/`decline`,
|
|
1276
|
+
`thumbnail` and `<decision>_refused`. `thumbnails` is the automatic thumbnail
|
|
1277
|
+
after a review publish: `captured`, `existing` (the app already had one),
|
|
1278
|
+
`superseded` (the creator's own thumbnail or a newer release won),
|
|
1279
|
+
`not_needed`, `owner_missing` or `failed`. `refusals` counts conflicts that
|
|
1280
|
+
rolled back, keyed `decision:code` (for example
|
|
1281
|
+
`approve:build_reward_review_stale`, `accept:build_reward_proposal_stale`).
|
|
1282
|
+
Report the totals. Each `failed` thumbnail and each `publish` without a
|
|
1283
|
+
`thumbnail` event on a completed day is a carry-over todo with the review
|
|
1284
|
+
ids (`reward-review show <id>` lists its events). An `accept` without a
|
|
1285
|
+
`proposal_viewed` for that revision means the creator accepted without opening
|
|
1286
|
+
the comparison; mention it, it is not a fault. Refusals are the concurrency
|
|
1287
|
+
guards working; report them, and escalate only a repeated pattern on one app.
|
|
1288
|
+
|
|
1242
1289
|
## Private carry-over todos
|
|
1243
1290
|
|
|
1244
1291
|
```bash
|
|
@@ -2273,6 +2320,21 @@ type PostSkip = Success<{
|
|
|
2273
2320
|
|
|
2274
2321
|
## Twinkle Newspaper
|
|
2275
2322
|
|
|
2323
|
+
**Audience (Mikey, 2026-09-15): children and young Twinkle users aged 10–15.**
|
|
2324
|
+
Choose and write stories for what these readers would voluntarily spend time
|
|
2325
|
+
reading. Before selecting a story, identify its appeal to them: curiosity,
|
|
2326
|
+
humor, a relatable experience, a creative idea, something useful, or a chance
|
|
2327
|
+
to join in. Games, art, puzzles, friendships, shared reflections, and community
|
|
2328
|
+
discussions can all supply good stories; read the actual source to find the
|
|
2329
|
+
substance.
|
|
2330
|
+
|
|
2331
|
+
Use clear, lively language that respects readers' intelligence. Give enough
|
|
2332
|
+
context for someone who missed the original post, and make the interesting
|
|
2333
|
+
part clear in the headline and opening. Avoid talking down to readers, forced
|
|
2334
|
+
slang, preachy lessons, administrative summaries, and blurbs that merely say
|
|
2335
|
+
someone uploaded or posted something. Keep the appeal grounded in the source;
|
|
2336
|
+
never invent excitement, reactions, popularity, or drama.
|
|
2337
|
+
|
|
2276
2338
|
```bash
|
|
2277
2339
|
lumine admin news --json
|
|
2278
2340
|
lumine admin news claim --output claim.json --scaffold editorial.json --json
|
|
@@ -2311,6 +2373,7 @@ re-checked transactionally at commit.
|
|
|
2311
2373
|
|
|
2312
2374
|
```ts
|
|
2313
2375
|
type GeneratedEditorial = {
|
|
2376
|
+
excludedSubjectEventKeys?: string[]; // experiment-video Subjects omitted entirely
|
|
2314
2377
|
mastheadHeadline: string;
|
|
2315
2378
|
mastheadDeck: string;
|
|
2316
2379
|
lead: {
|
|
@@ -2336,6 +2399,20 @@ nothing disappears silently: digest events the editorial does not account for
|
|
|
2336
2399
|
are added back. Two mechanisms make real curation possible within that
|
|
2337
2400
|
guarantee:
|
|
2338
2401
|
|
|
2402
|
+
**Exception — experiment videos (Mikey, 2026-09-15):** exclude these entirely,
|
|
2403
|
+
including school science-contest entries. The editor/model identifies them from
|
|
2404
|
+
the supplied context and lists their exact keys in `excludedSubjectEventKeys`.
|
|
2405
|
+
Do not cite, summarize, or group them into newspaper coverage. The API must have
|
|
2406
|
+
this exclusion support deployed before submitting such an editorial; older APIs
|
|
2407
|
+
ignore the field and restore the posts. The server accepts only canonical Subject
|
|
2408
|
+
keys and never lets this field remove official announcements. After excluding
|
|
2409
|
+
these posts, look for worthwhile replacement stories among the edition's eligible
|
|
2410
|
+
Subjects and shared Daily Reflections. A bounded digest dominated by experiment
|
|
2411
|
+
videos does not establish that the day has no other stories. Check the available
|
|
2412
|
+
canonical sources within the coverage window, and keep replacement stories within
|
|
2413
|
+
the claim and citation contract. Do not stop at deletion when suitable material
|
|
2414
|
+
is available, or invent filler to reach an article count.
|
|
2415
|
+
|
|
2339
2416
|
- **`coveredEventKeys`** — an arc story may list the other events it narrates
|
|
2340
2417
|
(an app's release + its update stream + its open-sourcing; one member's
|
|
2341
2418
|
related posts). Covered events are omitted from the layout — the arc IS
|
|
@@ -2368,17 +2445,17 @@ within the returned digest. On a typical day every front subject arrives with
|
|
|
2368
2445
|
the same priority, so treat a tied score (or recency) as no signal at all and
|
|
2369
2446
|
make the call by reading:
|
|
2370
2447
|
|
|
2371
|
-
- **Choose the lead
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2448
|
+
- **Choose the lead for readers aged 10–15.** Lead with the eligible front
|
|
2449
|
+
event whose substance is most likely to catch their interest and reward
|
|
2450
|
+
reading. A thoughtful conversation, a funny or relatable reflection, a
|
|
2451
|
+
striking creation, or an inviting community challenge can all qualify.
|
|
2452
|
+
Read the source and any supplied replies to understand the appeal; priority,
|
|
2453
|
+
recency, and the mere presence of an argument do not decide the lead.
|
|
2377
2454
|
- **Thread a theme through the paper.** Pick the strongest idea of the day
|
|
2378
2455
|
and let the masthead, the lead, and the editor's note all carry it, with
|
|
2379
|
-
the editor's note
|
|
2380
|
-
|
|
2381
|
-
|
|
2456
|
+
the editor's note leaving readers with an observation, question, or invitation
|
|
2457
|
+
grounded in one of the day's posts. Let a theme emerge from the material;
|
|
2458
|
+
keep each story's meaning intact and avoid forcing a moral lesson.
|
|
2382
2459
|
- **Cross-reference events into arcs.** The same thing often appears in the
|
|
2383
2460
|
digest several times (an app's release, its open-sourcing, and its maker's
|
|
2384
2461
|
Daily Reflection about it). Write those as one story arc — the origin
|
|
@@ -2594,6 +2671,24 @@ Read every line, and put these in the daily report verbatim:
|
|
|
2594
2671
|
line;
|
|
2595
2672
|
- the `day-over-day … worker_heap=…` delta.
|
|
2596
2673
|
|
|
2674
|
+
**The memory report looks back only 24 hours; the run's window is often longer.**
|
|
2675
|
+
On 2026-09-19 a four-day window hid two heap-OOM aborts (09-16 and 09-17) that
|
|
2676
|
+
`memory-daily` no longer showed. In every full review, also search the reviewed
|
|
2677
|
+
error log for `[cluster] worker exited` lines that are not planned or operator
|
|
2678
|
+
recycles, across the whole window since the last completed full run, and treat
|
|
2679
|
+
each one exactly like a non-zero `oom_aborts` / `unexpected_worker_exits`.
|
|
2680
|
+
|
|
2681
|
+
Each unexpected exit is followed by a `[cluster] worker last work slot=… pid=…
|
|
2682
|
+
in_flight=[…] recent=[…]` line (added 2026-09-19). The worker writes that trail
|
|
2683
|
+
synchronously as each piece of work begins and ends, so it survives an abort
|
|
2684
|
+
inside a single synchronous burst. `in_flight` names the requests or socket
|
|
2685
|
+
events that were running when the process died; `recent` lists the last
|
|
2686
|
+
sixteen begin/label/end records with their age before exit. Quote both lists
|
|
2687
|
+
verbatim in the report and in the todo: they are the evidence that pinpoints
|
|
2688
|
+
the code path. `unavailable (no trail file)` means the worker died before its
|
|
2689
|
+
trail existed or an older generation is still running; say so rather than
|
|
2690
|
+
guessing.
|
|
2691
|
+
|
|
2597
2692
|
Escalate in the report (a todo, and a note for Mikey) when any of these hold:
|
|
2598
2693
|
`oom_aborts` > 0, `service_restarts` > 0, `unexpected_worker_exits` > 0, any
|
|
2599
2694
|
worker `heap_high_water_pct` ≥ 95, or `heap_recycles` ≥ 4 in a day (the
|
|
@@ -2653,14 +2748,14 @@ The additive host-owner migration and compatible API must be live before this
|
|
|
2653
2748
|
CLI capability is published.
|
|
2654
2749
|
|
|
2655
2750
|
Review every participating host, including primary private-helper logs. A
|
|
2656
|
-
primary review does not cover the target.
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2751
|
+
primary review does not cover the target. An open review does not block a
|
|
2752
|
+
deployment or host hold. Its files, lease and database boundaries persist;
|
|
2753
|
+
active requests use the normal drain. A held or unavailable owner returns a
|
|
2754
|
+
retryable failure, so keep the session and retry when that host is available
|
|
2755
|
+
again. Release operators can review final shutdown deltas via management SSH
|
|
2756
|
+
and record their own evidence. An active review keeps ownership of clearing;
|
|
2757
|
+
otherwise API stderr is cleared with the existing guarded
|
|
2758
|
+
`npm run logs:clear-errors` plus post-clear re-read. A stopped target whose final logs
|
|
2664
2759
|
were reviewed does not need to be started for daily management; starting EC2
|
|
2665
2760
|
requires separate authority. See `twinkle-api/DEPLOY_TIME_HANDOFF.md`.
|
|
2666
2761
|
|
|
@@ -3035,9 +3130,16 @@ energy ledger (`chargedUsd`, `overflowUsd`, `users`, `recharges`; 1,000,000
|
|
|
3035
3130
|
units = $1), every telemetry counter (`busy_refusal`, `autofix_yielded`,
|
|
3036
3131
|
`autofix_superseded`, `reservation_admitted` with `avgRunBudgetUsd`,
|
|
3037
3132
|
`budget_stop_changed` / `budget_stop_unchanged` / `run_completed` with a
|
|
3038
|
-
per-model breakdown, `stop_settled`, `tool_limit_settled
|
|
3039
|
-
|
|
3040
|
-
|
|
3133
|
+
per-model breakdown, `stop_settled`, `tool_limit_settled`, and since
|
|
3134
|
+
2026-09-15 the queued-request counters `queued_restored` (a workspace reload
|
|
3135
|
+
restored its owner's still-queued request, shown with Stop), `queued_stopped`
|
|
3136
|
+
(Stop cancelled a request while it was still queued) and
|
|
3137
|
+
`busy_resume_requested` (after a busy refusal the website resumed the
|
|
3138
|
+
existing request)) and per-model per-run usage stats (`runs`, `callsPerRun`,
|
|
3139
|
+
`usdPerRun`, each avg and nearest-rank p90). `busy_refusal` with no
|
|
3140
|
+
`busy_resume_requested` on a day with website traffic means clients are not
|
|
3141
|
+
resuming the refused request; `queued_restored` shows how often creators
|
|
3142
|
+
reload while waiting in the queue. The current UTC day is returned with `inProgress: true`.
|
|
3041
3143
|
**Headline `lastCompletedDay` (its exact `dayKey`) — never the in-progress
|
|
3042
3144
|
day**, exactly as the closed-day AI-cost duty does.
|
|
3043
3145
|
|