@maka/maka-cli 5.189.0 → 5.191.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.189.0",
3
+ "version": "5.191.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",
@@ -7,6 +7,7 @@ import { Logger } from '../utilities/logger.js';
7
7
  import { clampDeviceKind } from '../utilities/affordances.js';
8
8
  import { GenerationCapture } from '../utilities/generation-capture.js';
9
9
  import { fetchCanonContext } from '../utilities/canon-lore.js';
10
+ import { providerUnavailable } from '../utilities/draft-failure-note.js';
10
11
  import { SceneSynthesizer } from './scene-factory.js';
11
12
  import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, VAULT_HOST_RATING, } from './scene-chunks.js';
12
13
  // The fixer name convention and the player dossier live with the prompt
@@ -169,6 +170,26 @@ export class SceneSeedGenerator {
169
170
  catch (err) {
170
171
  carriedError = err instanceof Error ? err.message : String(err);
171
172
  logger.write(`SceneSeedGenerator: pipeline ${pipeline} failed: ${carriedError}`);
173
+ // A PROVIDER THAT IS NOT ANSWERING WILL NOT ANSWER THREE TIMES
174
+ // (po7qSJycJrcfWbgaA, 2026-09-14). The pipeline budget exists for
175
+ // a model that wrote a bad SCENE -- a fresh skeleton is a real
176
+ // second chance at that. It is worth nothing against a provider
177
+ // that never started: DeepSeek refused this run's generation with
178
+ // "unable to start processing your request within the 900-second
179
+ // timeout limit", and the loop dutifully spent that wait twice
180
+ // more. Three-quarters of an hour, for a player standing in the
181
+ // hub, to arrive at the same refusal the first attempt already
182
+ // had in hand.
183
+ //
184
+ // So an unavailable provider ends the generation instead of
185
+ // re-buying it. The message survives in carriedError and reaches
186
+ // the player through draftFailureNote, which tells them it is the
187
+ // provider and not their key -- which is the whole point of
188
+ // stopping early rather than looking flaky for 45 minutes.
189
+ if (providerUnavailable(carriedError)) {
190
+ logger.write(`SceneSeedGenerator: provider is not answering -- abandoning the remaining ${this.maxPipelineAttempts - pipeline} pipeline attempt(s).`);
191
+ break;
192
+ }
172
193
  }
173
194
  }
174
195
  logger.write(`SceneSeedGenerator: giving up (${carriedError ?? 'unknown'}) -- falling back to a static scene`);
@@ -188,7 +209,7 @@ export class SceneSeedGenerator {
188
209
  let lastError = previousError;
189
210
  for (let attempt = 1; attempt <= this.maxCallAttempts; attempt++) {
190
211
  const prompt = buildSkeletonPrompt(tier, player, lastError, rumor, crew, canonBlock, runType);
191
- const reply = await AI.ask(prompt, { maxTokens: this.SKELETON_TOKENS });
212
+ const reply = await AI.ask(prompt, { maxTokens: this.SKELETON_TOKENS, timeoutMs: AI.GENERATION_TIMEOUT_MS });
192
213
  try {
193
214
  const skeleton = parseJsonReply(reply);
194
215
  skeleton.devices = skeleton.devices ?? [];
@@ -231,7 +252,7 @@ export class SceneSeedGenerator {
231
252
  const fullPrompt = lastError
232
253
  ? `${prompt}\n\nYour previous attempt failed with this error -- fix it and try again: ${lastError}`
233
254
  : prompt;
234
- const reply = await AI.ask(fullPrompt, { maxTokens });
255
+ const reply = await AI.ask(fullPrompt, { maxTokens, timeoutMs: AI.GENERATION_TIMEOUT_MS });
235
256
  try {
236
257
  const parsed = parseJsonReply(reply);
237
258
  logger.write(`SceneSeedGenerator: chunk "${label}" ok (attempt ${attempt})`);
@@ -191,7 +191,7 @@ import { clockTime, worldNow } from './utilities/world-clock.js';
191
191
  import { CommandFactory } from './factories/command-factory.js';
192
192
  import { SceneSynthesizer } from './factories/scene-factory.js';
193
193
  import { SceneSeedGenerator, FIXER_NAME, isFixerName } from './factories/scene-seed-generator.js';
194
- import { draftFailureNote } from './utilities/draft-failure-note.js';
194
+ import { draftFailureNote, providerUnavailable } from './utilities/draft-failure-note.js';
195
195
  import { CALL_ICON, CALL_COLOR, END_CALL_SENTINEL } from './utilities/comm-style.js';
196
196
  import { AI } from '../../../tools/ai/ai.class.js';
197
197
  import { BULLET } from './utilities/log-style.js';
@@ -3843,16 +3843,99 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3843
3843
  * caller, of which there are three.
3844
3844
  */
3845
3845
  draftNextSceneInBackground(runType) {
3846
+ // WHOSE CALL THIS IS, CAPTURED NOW (po7qSJycJrcfWbgaA: "I haven't
3847
+ // been getting a call from mr. krow back after a while").
3848
+ //
3849
+ // Two ways this promise used to end in silence, and the player
3850
+ // cannot tell them apart from the outside -- they are just still
3851
+ // waiting for a call that never comes.
3852
+ //
3853
+ // 1. A THROW WAS INVISIBLE. `.catch` wrote to the debug log and
3854
+ // nothing reached the screen, so any unexpected failure in a
3855
+ // 20-40 second draft cost the player the callback with no line
3856
+ // and no way to ask again knowingly. The comment in
3857
+ // draftNextScene already promised the opposite -- "the wait
3858
+ // resolves as HIS callback either way, success or failure" --
3859
+ // and that was true of a RETURNED failure and false of a thrown
3860
+ // one. An empty string fell through the same hole.
3861
+ //
3862
+ // 2. THE AMBIENT SCOPE WAS GONE BY THE TIME IT RESOLVED. Log lines
3863
+ // scope themselves off `currentSession().dispatch` (headless.ts
3864
+ // ambient), and handleInputAs MUTATES that field for the length
3865
+ // of one command and restores it in a `finally`. This promise
3866
+ // outlives its command by half a minute: at best `dispatch` is
3867
+ // undefined by then and the ping goes to the whole table; at
3868
+ // worst ANOTHER player at the table is mid-command and the ping
3869
+ // is scoped to THEIR room -- so the runner who made the call is
3870
+ // the one person who does not hear it back. Capturing the
3871
+ // caller's name while we are still inside the dispatch, and
3872
+ // scoping to them, removes the dependency on state that is torn
3873
+ // down before the answer arrives.
3874
+ //
3875
+ // Scoped to the CALLER and not the room: this is a phone call to
3876
+ // one runner, and it lands on their link wherever they are standing
3877
+ // by the time it comes back.
3878
+ //
3879
+ // The WHO and the CHANNEL are both captured here, while the call is
3880
+ // still being made, rather than looked up when the answer lands.
3881
+ // Logger.getInstance() resolves off the same async context as the
3882
+ // dispatch above, and this ping is delivered from a timer as well as
3883
+ // from a promise -- so reaching for it later means depending on that
3884
+ // context surviving into whichever of the two gets there first. It
3885
+ // is one session and one logger for the whole call; take both now.
3886
+ const caller = currentSession()?.dispatch?.actorName;
3887
+ const logger = Logger.getInstance();
3888
+ let spoke = false;
3889
+ const ring = (text) => {
3890
+ spoke = true;
3891
+ logger.logWithColor(`${CALL_ICON} ${text}`, CALL_COLOR, caller ? { actor: caller } : undefined);
3892
+ };
3893
+ // 3. AND A PROMISE THAT NEVER SETTLES SAYS NOTHING AT ALL. The two
3894
+ // holes above are both about an answer that arrived and was
3895
+ // dropped; this is the one where none arrives. The draft awaits a
3896
+ // chain of AI calls and, for a crew job, a 300-second server
3897
+ // round trip -- and the reporter's own server log shows the kind
3898
+ // of thing that stalls one (a presence change stream 30 seconds
3899
+ // behind). Whatever hangs, the player is left waiting on a call
3900
+ // with no deadline, which is the report word for word.
3901
+ //
3902
+ // So the wait has an end. DELIBERATELY LONGER than the worst
3903
+ // legitimate draft -- the shared-session call alone is allowed
3904
+ // 300s (shared-run.ts, and 150s was measured too short) and the
3905
+ // chunked generation runs minutes on top -- because a watchdog
3906
+ // that fires while the job is still landing would replace a real
3907
+ // offer with "nothing shook loose", which is worse than the
3908
+ // silence it set out to fix. If the draft DOES land after this
3909
+ // has spoken, the job is still rung through: good news is worth
3910
+ // saying twice, and the player is owed the run they asked for.
3911
+ //
3912
+ // unref'd so a pending watchdog never holds the process open --
3913
+ // a solo CLI session must still exit on "quit".
3914
+ const watchdog = setTimeout(() => {
3915
+ if (!spoke) {
3916
+ ring(`${FIXER_NAME}: "Still nothing I can put your name to, chummer. Call me again in a bit."`);
3917
+ }
3918
+ }, Game.DRAFT_PATIENCE_MS);
3919
+ watchdog.unref?.();
3846
3920
  void this.continueToNextScene(runType)
3847
3921
  .then(nextJobText => {
3848
- if (nextJobText) {
3849
- Logger.getInstance().logWithColor(`${CALL_ICON} ${nextJobText}`, CALL_COLOR);
3850
- }
3922
+ clearTimeout(watchdog);
3923
+ if (nextJobText)
3924
+ ring(nextJobText);
3925
+ else if (!spoke)
3926
+ ring(`${FIXER_NAME}: "Nothing shook loose, chummer. Call me again in a bit."`);
3851
3927
  })
3852
3928
  .catch(err => {
3853
- Logger.getInstance().error(`Failed to draft the next job: ${err}`);
3929
+ clearTimeout(watchdog);
3930
+ logger.error(`Failed to draft the next job: ${err}`);
3931
+ if (!spoke)
3932
+ ring(`${FIXER_NAME}: "Job fell apart on my end, chummer -- don't ask. Give me another call in a bit."`);
3854
3933
  });
3855
3934
  }
3935
+ /** How long the runner waits on Krow before he rings back empty-handed.
3936
+ * See the watchdog in draftNextSceneInBackground for why it is this
3937
+ * far past the worst honest draft rather than close to the typical one. */
3938
+ static DRAFT_PATIENCE_MS = 6 * 60 * 1000;
3856
3939
  /**
3857
3940
  * Drafts and loads the next job once the current scene's win condition
3858
3941
  * has been met. Reuses the same long-lived Player (and its inventory),
@@ -4563,8 +4646,25 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4563
4646
  // caller but the ask-for-data-work door passes.
4564
4647
  const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, this.player, this.activeRumor, localCrew, undefined, runType);
4565
4648
  if (!nextSeed) {
4566
- const opsNote = draftFailureNote(SceneSeedGenerator.lastFailure ?? '');
4567
- return `Incoming call -- ${FIXER_NAME}: "Job fell through, chummer. Client got cold feet before the ink dried. Give me another call in a bit -- I'll shake something loose."${opsNote}`;
4649
+ const why = SceneSeedGenerator.lastFailure ?? '';
4650
+ const opsNote = draftFailureNote(why);
4651
+ // DO NOT INVENT A CLIENT WHO GOT COLD FEET (po7qSJycJrcfWbgaA).
4652
+ // That line is fine for a draft that came back malformed -- the
4653
+ // fiction has to say something, and "the deal soured" is as true
4654
+ // as anything. It is a LIE when the provider never answered: it
4655
+ // names a cause inside the story for something that happened
4656
+ // entirely outside it, and the advice it gives ("call me in a
4657
+ // bit") walks the player straight back into the same wall. This
4658
+ // codebase refuses invented causes everywhere else -- a fixer may
4659
+ // not name a venue it does not know, an NPC may not name a runner
4660
+ // who is not there -- and the rule holds here too.
4661
+ //
4662
+ // So an outage gets its own line: Krow cannot reach anyone, which
4663
+ // is the honest shape of it in his voice, and the OOC note under
4664
+ // it says the rest.
4665
+ return providerUnavailable(why)
4666
+ ? `Incoming call -- ${FIXER_NAME}: "Can't raise anybody tonight, chummer -- whole street's gone quiet on me. Sit tight; I'll call when the lines come back."${opsNote}`
4667
+ : `Incoming call -- ${FIXER_NAME}: "Job fell through, chummer. Client got cold feet before the ink dried. Give me another call in a bit -- I'll shake something loose."${opsNote}`;
4568
4668
  }
4569
4669
  // HUB sessions: the draft lands as a MESSAGE, never a teleport (real
4570
4670
  // playtest note: being yanked into the run the instant generation
@@ -23,6 +23,30 @@
23
23
  * codes need word boundaries so they can't match inside some other
24
24
  * number.
25
25
  */
26
+ /**
27
+ * IS THE PROVIDER SIMPLY NOT ANSWERING? Exported because two decisions
28
+ * hang off the same question and must never disagree: what the player is
29
+ * told (below), and whether SceneSeedGenerator bothers with its remaining
30
+ * pipeline attempts (po7qSJycJrcfWbgaA). Two separately-written lists
31
+ * would drift, and the drift would be invisible -- the generator would
32
+ * keep re-buying a fifteen-minute wait while the note said the provider
33
+ * was down.
34
+ *
35
+ * Matched on PROSE, not status codes: only the prose crosses the wire,
36
+ * and each provider words it differently.
37
+ *
38
+ * The 2026-09-14 addition is DeepSeek's own queue refusal -- "We were
39
+ * unable to start processing your request within the 900-second timeout
40
+ * limit. Please try again later." Nothing in the old list came close to
41
+ * it, so a real outage read to the player as "Job fell through, chummer.
42
+ * Client got cold feet" with no note at all, and sent them straight back
43
+ * to call into the same wall. `Request timeout` is this codebase's own
44
+ * transport giving up (https.class.ts), which means the same thing from
45
+ * the other end of the socket.
46
+ */
47
+ export function providerUnavailable(why) {
48
+ return /overloaded or down|rate-limited by its provider|took too long to answer|could not (?:complete that request|reach its ai provider)|unable to start processing|\b\d{2,4}-second timeout limit|Request timeout|ETIMEDOUT|ECONNRESET|socket hang up/i.test(why);
49
+ }
26
50
  export function draftFailureNote(why) {
27
51
  // NAME THE CAUSE WHEN IT'S THE PROVIDER, NOT LUCK (real session: six
28
52
  // Krow calls into an out-of-credits key, indistinguishable from flaky
@@ -50,7 +74,7 @@ export function draftFailureNote(why) {
50
74
  // answering. Matched on the SITE'S OWN wording (ai-error-envelope.ts
51
75
  // classifyUpstream/classifyTransportFailure) rather than status
52
76
  // codes, since only the prose crosses the wire.
53
- const providerOutage = !providerDown && /overloaded or down|rate-limited by its provider|took too long to answer|could not (?:complete that request|reach its ai provider)/i.test(why);
77
+ const providerOutage = !providerDown && providerUnavailable(why);
54
78
  // A structural failure is OURS, not the player's -- name it as the
55
79
  // generator's problem so nobody goes hunting their billing page, and
56
80
  // keep it short: the fiction already covers the beat.
@@ -377,6 +377,27 @@ export class AI {
377
377
  * runs 3-5x a dialogue reply, and the old fixed caps truncated it
378
378
  * mid-string). Leave unset for ordinary dialogue-sized replies.
379
379
  */
380
+ /**
381
+ * HOW LONG A GENERATION CALL WAITS (po7qSJycJrcfWbgaA, 2026-09-14).
382
+ *
383
+ * DeepSeek answered a run's scene generation with "We were unable to
384
+ * start processing your request within the 900-second timeout limit"
385
+ * -- fifteen minutes of queue before a refusal, and the generator's
386
+ * pipeline budget took that three times over. Forty-five minutes of a
387
+ * player standing in the hub waiting for a call that, when it finally
388
+ * came, said the job fell through.
389
+ *
390
+ * The transport's own default is the module's TIMEOUT (300s), which
391
+ * is a sane ceiling for a big download and far too patient for a
392
+ * prompt somebody is waiting on. A scene chunk that has not started
393
+ * coming back in 90 seconds is not going to save the commission.
394
+ *
395
+ * Deliberately NOT applied to every AI.ask: dialogue calls are short
396
+ * and already bounded by the same default, and an NPC that takes an
397
+ * extra beat is not a player staring at nothing. Generation asks for
398
+ * it explicitly.
399
+ */
400
+ static GENERATION_TIMEOUT_MS = 90_000;
380
401
  static async ask(prompt, opts) {
381
402
  const config = this.resolveConfig();
382
403
  if (!config) {
@@ -384,13 +405,14 @@ export class AI {
384
405
  'or log in to maka-cli.com (`maka login`) to use the server relay ' +
385
406
  '(optionally MAKA_AI_PROVIDER to choose explicitly).');
386
407
  }
408
+ const timeoutMs = opts?.timeoutMs;
387
409
  const result = config.provider === 'openai'
388
- ? await HTTPS.askGPT({ prompt, openApiKey: config.apiKey, model: config.model, maxTokens: opts?.maxTokens })
410
+ ? await HTTPS.askGPT({ prompt, openApiKey: config.apiKey, model: config.model, maxTokens: opts?.maxTokens, timeoutMs })
389
411
  : config.provider === 'anthropic'
390
- ? await HTTPS.askClaude({ prompt, apiKey: config.apiKey, model: config.model, maxTokens: opts?.maxTokens })
412
+ ? await HTTPS.askClaude({ prompt, apiKey: config.apiKey, model: config.model, maxTokens: opts?.maxTokens, timeoutMs })
391
413
  : config.provider === 'deepseek'
392
- ? await HTTPS.askDeepSeek({ prompt, apiKey: config.apiKey, model: config.model, maxTokens: opts?.maxTokens })
393
- : await HTTPS.askMakaServer({ prompt, authToken: config.apiKey, model: config.model, maxTokens: opts?.maxTokens });
414
+ ? await HTTPS.askDeepSeek({ prompt, apiKey: config.apiKey, model: config.model, maxTokens: opts?.maxTokens, timeoutMs })
415
+ : await HTTPS.askMakaServer({ prompt, authToken: config.apiKey, model: config.model, maxTokens: opts?.maxTokens, timeoutMs });
394
416
  // A BODY THAT IS NOT JSON IS AN HTTP PROBLEM, NOT A PARSE PROBLEM.
395
417
  // This parse used to run bare, AHEAD of the statusCode guard below
396
418
  // -- so that guard only ever protected JSON-shaped failures. When
@@ -258,7 +258,7 @@ export class HTTPS {
258
258
  req.end();
259
259
  });
260
260
  }
261
- static async askGPT({ prompt, openApiKey, certPath, model = 'gpt-4.1', maxTokens = 3000 }) {
261
+ static async askGPT({ prompt, openApiKey, certPath, model = 'gpt-4.1', maxTokens = 3000, timeoutMs }) {
262
262
  const headers = {
263
263
  'Authorization': `Bearer ${openApiKey}`,
264
264
  'Content-Type': 'application/json',
@@ -283,10 +283,11 @@ export class HTTPS {
283
283
  path: '/v1/chat/completions',
284
284
  data,
285
285
  headers,
286
- cert: certPath
286
+ cert: certPath,
287
+ timeout: timeoutMs
287
288
  });
288
289
  }
289
- static async askClaude({ prompt, apiKey, certPath, model = 'claude-sonnet-5', maxTokens = 4096 }) {
290
+ static async askClaude({ prompt, apiKey, certPath, model = 'claude-sonnet-5', maxTokens = 4096, timeoutMs }) {
290
291
  const headers = {
291
292
  'x-api-key': apiKey,
292
293
  'anthropic-version': '2023-06-01',
@@ -328,7 +329,8 @@ export class HTTPS {
328
329
  path: '/v1/messages',
329
330
  data,
330
331
  headers,
331
- cert: certPath
332
+ cert: certPath,
333
+ timeout: timeoutMs
332
334
  });
333
335
  }
334
336
  /**
@@ -349,7 +351,7 @@ export class HTTPS {
349
351
  * reasoning tokens that never reach the player. The same trap the
350
352
  * Claude path above documents, sprung by a different vendor.
351
353
  */
352
- static async askDeepSeek({ prompt, apiKey, certPath, model = 'deepseek-v4-flash', maxTokens = 4096 }) {
354
+ static async askDeepSeek({ prompt, apiKey, certPath, model = 'deepseek-v4-flash', maxTokens = 4096, timeoutMs }) {
353
355
  return await this.post({
354
356
  hostname: 'api.deepseek.com',
355
357
  path: '/anthropic/v1/messages',
@@ -364,7 +366,8 @@ export class HTTPS {
364
366
  'anthropic-version': '2023-06-01',
365
367
  'Content-Type': 'application/json',
366
368
  },
367
- cert: certPath
369
+ cert: certPath,
370
+ timeout: timeoutMs
368
371
  });
369
372
  }
370
373
  /**
@@ -375,7 +378,7 @@ export class HTTPS {
375
378
  * resume token, not an API key. Production host only -- the relay is
376
379
  * an account nicety, not a dev-loop dependency.
377
380
  */
378
- static async askMakaServer({ prompt, authToken, model, maxTokens = 4096 }) {
381
+ static async askMakaServer({ prompt, authToken, model, maxTokens = 4096, timeoutMs }) {
379
382
  // MAKA_DEV TARGETING (kvhsgn3ouEG2znsha). Every other maka-cli.com
380
383
  // transport in this codebase already swings to a local server --
381
384
  // backlog, canon-lore, catalog, cloud-saves, session-telemetry,
@@ -401,8 +404,10 @@ export class HTTPS {
401
404
  data: { prompt, model, maxTokens },
402
405
  headers: { 'x-auth-token': authToken },
403
406
  // Generation-length calls ride this too; outlive the server's own
404
- // 150s upstream timeout so its error body reaches us.
405
- timeout: 180000,
407
+ // 150s upstream timeout so its error body reaches us. A caller
408
+ // asking for LESS patience still wins -- it is the one that knows
409
+ // whether somebody is waiting on the answer.
410
+ timeout: timeoutMs ?? 180000,
406
411
  });
407
412
  }
408
413
  static async askGPTStreaming(prompt, model = 'gpt-4.1') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.189.0",
3
+ "version": "5.191.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",