@cryer/star-cli 0.3.3 → 0.3.5

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.
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  AgentLoop,
4
4
  PLAN_MODE_PROMPT
5
- } from "./chunk-3L6QKJM4.js";
5
+ } from "./chunk-OIDB2E5J.js";
6
6
  import "./chunk-B6P4VUBE.js";
7
7
  export {
8
8
  AgentLoop,
9
9
  PLAN_MODE_PROMPT
10
10
  };
11
- //# sourceMappingURL=loop-TQ7GFTTE.js.map
11
+ //# sourceMappingURL=loop-H5RYTO2O.js.map
package/dist/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AgentLoop,
4
+ ConfigSchema,
4
5
  MAX_IMAGE_DIMENSION,
5
6
  SKIP_DIRS,
6
7
  TodoStore,
@@ -13,6 +14,7 @@ import {
13
14
  collectCommitContext,
14
15
  collectWorkingDiff,
15
16
  compactMessages,
17
+ contextWindowTokens,
16
18
  createDefaultRegistry,
17
19
  createIgnorePredicate,
18
20
  defaultAgentTasks,
@@ -20,6 +22,7 @@ import {
20
22
  diffTreeNames,
21
23
  discoverSkills,
22
24
  estimateTokens,
25
+ formatAgentTaskUpdate,
23
26
  formatTaskFinished,
24
27
  formatTaskList,
25
28
  formatTaskStarted,
@@ -41,7 +44,7 @@ import {
41
44
  resolveSnapshotContent,
42
45
  rewindToSnapshot,
43
46
  summarizeMessages
44
- } from "./chunk-3L6QKJM4.js";
47
+ } from "./chunk-OIDB2E5J.js";
45
48
  import {
46
49
  envFilePath,
47
50
  globalConfigPath,
@@ -598,113 +601,6 @@ async function savePermissionMode(mode) {
598
601
  await writeFileAtomic(filePath, upsertTopLevelKeyText(content, "permissionMode", mode));
599
602
  }
600
603
 
601
- // src/config/schema.ts
602
- import { z } from "zod";
603
- var ProviderConfigSchema = z.object({
604
- name: z.string(),
605
- // openai-responses talks to the Responses API (/responses) instead of chat
606
- // completions — the wire format codex uses, and the native API of
607
- // reasoning-model relays that otherwise hide reasoning behind empty chat
608
- // completions. streamChat sends strictSchemas:false + store:false with it.
609
- protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
610
- baseURL: z.string(),
611
- apiKeyEnv: z.string().optional(),
612
- apiKey: z.string().optional(),
613
- headers: z.record(z.string()).optional()
614
- });
615
- var ModelConfigSchema = z.object({
616
- name: z.string(),
617
- provider: z.string(),
618
- model: z.string(),
619
- maxTokens: z.number().int().positive().optional(),
620
- // Per-model context window; overrides the top-level contextMaxTokens for
621
- // compaction and the status bar when set.
622
- contextMaxTokens: z.number().int().positive().optional(),
623
- // Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
624
- // the model is treated as unpriced.
625
- promptPrice: z.number().nonnegative().optional(),
626
- completionPrice: z.number().nonnegative().optional(),
627
- // Optional price (USD per 1M tokens) for cache-read prompt tokens. When
628
- // unset, OpenAI-style cached tokens bill at promptPrice and Anthropic-style
629
- // cache reads are not billed — set this for accurate cache pricing.
630
- cacheReadPrice: z.number().nonnegative().optional()
631
- });
632
- var PermissionsConfigSchema = z.object({
633
- allow: z.array(z.string()).default([]),
634
- deny: z.array(z.string()).default([])
635
- });
636
- var HookConfigSchema = z.object({
637
- event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
638
- matcher: z.string().refine(
639
- (pattern) => {
640
- try {
641
- new RegExp(pattern);
642
- return true;
643
- } catch {
644
- return false;
645
- }
646
- },
647
- { message: "invalid regular expression" }
648
- ).optional(),
649
- command: z.string().min(1),
650
- timeoutSec: z.number().positive().default(30)
651
- });
652
- var ConfigSchema = z.object({
653
- defaultModel: z.string().default(""),
654
- permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
655
- providers: z.array(ProviderConfigSchema).default([]),
656
- models: z.array(ModelConfigSchema).default([]),
657
- maxSteps: z.number().int().positive().default(50),
658
- contextMaxTokens: z.number().int().positive().default(1e5),
659
- // Seconds without any stream part before a stalled response is ended
660
- // gracefully (some relays never send the terminal chunks). Applies once
661
- // streaming has started; the first part gets a longer, fixed allowance.
662
- streamIdleTimeoutSec: z.number().positive().default(20),
663
- // Seconds to wait for the very first stream part before giving up; slow
664
- // thinking-model relays can buffer for minutes before sending anything.
665
- streamFirstChunkTimeoutSec: z.number().positive().default(300),
666
- // Extra attempts per model request when a stream fails transiently
667
- // (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
668
- // relay hiccup does not silently end a half-finished turn. 0 disables.
669
- // The wait between attempts honors Retry-After headers and otherwise backs
670
- // off exponentially with jitter (llm/retry.ts); retries after a
671
- // timeout/empty failure also scale both stream timeouts up (attempt number
672
- // ×, capped at 3x) since the relay is likely overloaded.
673
- streamMaxRetries: z.number().int().min(0).default(3),
674
- // How many consecutive unproductive replies may be nudged before the turn
675
- // is handed back: a text-only reply that announces pending work ("我将…",
676
- // "I will…") or answers a nudge with more words, or an empty reply that
677
- // survived the stream retry budget (steered with a nudge — a changed
678
- // request — since identical resends already failed). Tool calls reset the
679
- // count, so a productive task keeps going; exhaustion surfaces a notice
680
- // instead of stopping silently. Never fires in plan mode. 0 disables.
681
- maxAutoContinues: z.number().int().min(0).default(2),
682
- contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
683
- // Optional per-session dollar budget; when set, the REPL stops the session
684
- // once the estimated cost for the session reaches this amount.
685
- sessionBudgetUsd: z.number().positive().optional(),
686
- // Terminal bell (REPL only): ring when a turn takes longer than
687
- // notifyBellThresholdSec, and when a background task finishes.
688
- // STAR_NO_NOTIFY=1 disables without touching the config.
689
- notifyBell: z.boolean().default(true),
690
- notifyBellThresholdSec: z.number().positive().default(10),
691
- permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
692
- hooks: z.array(HookConfigSchema).default([]),
693
- // Consecutive identical tool calls (same tool + same arguments) allowed
694
- // before the loop refuses to execute the repeat and tells the model to
695
- // change approach — a guard against agents stuck retrying a failing call
696
- // (opencode's doom_loop). 0 disables.
697
- doomLoopThreshold: z.number().int().min(0).default(3),
698
- // Track the whole working tree with an internal git repo (outside the
699
- // user's .git) at every turn boundary, so /undo and /redo can restore
700
- // changes made by ANY tool (bash included), not just write_file/edit_file.
701
- // Falls back silently to per-file snapshots when git is unavailable.
702
- gitSnapshots: z.boolean().default(true)
703
- });
704
- function contextWindowTokens(config, modelName) {
705
- return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
706
- }
707
-
708
604
  // src/llm/provider.ts
709
605
  import { createAnthropic } from "@ai-sdk/anthropic";
710
606
  import { createOpenAI } from "@ai-sdk/openai";
@@ -931,6 +827,12 @@ function debugWarn(message) {
931
827
  if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
932
828
  `);
933
829
  }
830
+ var META_WRITE_MAX_ATTEMPTS = 5;
831
+ var META_WRITE_RETRY_DELAY_MS = 25;
832
+ function isTransientFsError(error) {
833
+ const code = error.code;
834
+ return code === "EPERM" || code === "EACCES" || code === "EBUSY";
835
+ }
934
836
  var FALLBACK_META = /* @__PURE__ */ Symbol("star.fallbackMeta");
935
837
  function isFallbackMeta(meta) {
936
838
  return meta[FALLBACK_META] === true;
@@ -1005,17 +907,33 @@ var SessionStore = class _SessionStore {
1005
907
  // writers race the loop's own appends, and a truncated read once turned
1006
908
  // fallbackMeta's empty defaults into a persisted wipe of model/cwd/usage.
1007
909
  // A branded fallback meta is never written back for the same reason.
910
+ // A write that still fails after retries is dropped rather than thrown:
911
+ // meta.json is bookkeeping the cached meta rebuilds on the next write,
912
+ // while the conversation itself lives in messages.jsonl — killing the
913
+ // agent turn over a stale updatedAt would be the wrong trade.
1008
914
  writeMeta(meta) {
1009
915
  if (isFallbackMeta(meta)) return Promise.resolve();
1010
916
  const tmp = `${this.metaPath()}.tmp-${process.pid}-${this.metaTmpSeq++}`;
1011
917
  const run = this.metaWriteQueue.then(async () => {
1012
- try {
1013
- await fs3.writeFile(tmp, JSON.stringify(meta, null, 2));
1014
- await fs3.rename(tmp, this.metaPath());
1015
- } catch (error) {
1016
- await fs3.unlink(tmp).catch(() => {
1017
- });
1018
- throw error;
918
+ for (let attempt = 0; attempt < META_WRITE_MAX_ATTEMPTS; attempt++) {
919
+ try {
920
+ await fs3.writeFile(tmp, JSON.stringify(meta, null, 2));
921
+ await fs3.rename(tmp, this.metaPath());
922
+ return;
923
+ } catch (error) {
924
+ if (isTransientFsError(error) && attempt + 1 < META_WRITE_MAX_ATTEMPTS) {
925
+ await new Promise(
926
+ (resolve) => setTimeout(resolve, META_WRITE_RETRY_DELAY_MS * (attempt + 1))
927
+ );
928
+ continue;
929
+ }
930
+ await fs3.unlink(tmp).catch(() => {
931
+ });
932
+ debugWarn(
933
+ `session ${this.id}: meta write failed (${error.code ?? String(error)}), skipping`
934
+ );
935
+ return;
936
+ }
1019
937
  }
1020
938
  });
1021
939
  this.metaWriteQueue = run.catch(() => {
@@ -1688,10 +1606,10 @@ async function compactSession(opts) {
1688
1606
  };
1689
1607
  }
1690
1608
  const beforeTokens = estimateTokens(messages);
1691
- const compacted = compactMessages(messages, config.contextMaxTokens);
1609
+ const compacted = compactMessages(messages, config.contextMaxTokens, { force: true });
1692
1610
  if (!compacted.compacted) {
1693
1611
  return {
1694
- message: `Nothing to compact: ~${beforeTokens} estimated tokens, below the limit of ${config.contextMaxTokens}.`,
1612
+ message: `Nothing to compact: history is too short to drop a whole turn (~${beforeTokens} estimated tokens).`,
1695
1613
  compacted: false
1696
1614
  };
1697
1615
  }
@@ -5003,11 +4921,7 @@ function Repl({
5003
4921
  };
5004
4922
  const onAgentUpdate = (task) => {
5005
4923
  setBgLabels(runningTaskLabels());
5006
- const label = task.description ? `: ${toTerminalSafe(task.description)}` : "";
5007
- pushMessage(
5008
- "system",
5009
- task.status === "running" ? `Background subagent ${task.id} started${label}` : `Background subagent ${task.id} ${task.status}${label}`
5010
- );
4924
+ pushMessage("system", toTerminalSafe(formatAgentTaskUpdate(task)));
5011
4925
  if (task.status !== "running") {
5012
4926
  notifyBell({
5013
4927
  enabled: configRef.current.notifyBell,
@@ -5638,18 +5552,26 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
5638
5552
  } catch {
5639
5553
  summaryModel = null;
5640
5554
  }
5641
- const result = await compactSession({
5642
- backend: backendRef.current,
5643
- sessionStore: sessionStoreRef.current,
5644
- config,
5645
- model: summaryModel
5646
- });
5647
- if (result.compacted && result.messages) {
5648
- const display = buildDisplayMessages(result.messages);
5649
- nextIdRef.current = display.length;
5650
- applyMessages(display);
5555
+ tickerStopRef.current = startTicker((tick) => setSpinnerTick(tick));
5556
+ setActivity("compacting context\u2026");
5557
+ try {
5558
+ const result = await compactSession({
5559
+ backend: backendRef.current,
5560
+ sessionStore: sessionStoreRef.current,
5561
+ config,
5562
+ model: summaryModel
5563
+ });
5564
+ if (result.compacted && result.messages) {
5565
+ const display = buildDisplayMessages(result.messages);
5566
+ nextIdRef.current = display.length;
5567
+ applyMessages(display);
5568
+ }
5569
+ return result.message;
5570
+ } finally {
5571
+ tickerStopRef.current?.();
5572
+ tickerStopRef.current = null;
5573
+ setActivity(null);
5651
5574
  }
5652
- return result.message;
5653
5575
  },
5654
5576
  exportSession: (arg) => exportSession({
5655
5577
  backend: backendRef.current,
@@ -5819,8 +5741,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5819
5741
  `permissionMode: ${config.permissionMode}`,
5820
5742
  `providers (${config.providers.length}): ${config.providers.map((p) => p.name).join(", ") || "(none)"}`,
5821
5743
  `models (${config.models.length}): ${config.models.map((m) => m.name).join(", ") || "(none)"}`,
5822
- `maxSteps: ${config.maxSteps}`,
5744
+ `maxSteps: ${config.maxSteps === 0 ? "unlimited" : config.maxSteps}`,
5823
5745
  `contextMaxTokens: ${config.contextMaxTokens}`,
5746
+ `compactThresholdTokens: ${config.compactThresholdTokens ?? "(context window)"}`,
5824
5747
  `streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
5825
5748
  `streamFirstChunkTimeoutSec: ${config.streamFirstChunkTimeoutSec}`,
5826
5749
  `streamMaxRetries: ${config.streamMaxRetries}`,
@@ -6099,6 +6022,7 @@ var PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
6099
6022
  "models",
6100
6023
  "maxSteps",
6101
6024
  "contextMaxTokens",
6025
+ "compactThresholdTokens",
6102
6026
  "streamIdleTimeoutSec",
6103
6027
  "streamFirstChunkTimeoutSec",
6104
6028
  "streamMaxRetries",