scream-code 0.15.1 → 0.15.2

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.
@@ -34205,14 +34205,27 @@ function convertMediaUrl(url, fallbackMimeType) {
34205
34205
  function createAbortError() {
34206
34206
  return new DOMException("The operation was aborted.", "AbortError");
34207
34207
  }
34208
- async function abortPromise(signal) {
34209
- if (signal === void 0) return new Promise(() => {});
34208
+ /**
34209
+ * Race async work against an abort signal, ALWAYS detaching the listener.
34210
+ * The Google GenAI SDK does not accept an AbortSignal, so callers race the
34211
+ * SDK call against the signal manually. A bare `addEventListener({ once })`
34212
+ * inside a losing promise leaks the listener on the caller's signal for the
34213
+ * rest of its lifetime - with a session-scoped signal that accumulates one
34214
+ * listener per request. This wrapper guarantees cleanup in both outcomes.
34215
+ */
34216
+ async function abortRace(signal, work) {
34217
+ if (signal === void 0) return work;
34210
34218
  if (signal.aborted) throw createAbortError();
34211
- return new Promise((_, reject) => {
34212
- signal.addEventListener("abort", () => {
34213
- reject(createAbortError());
34214
- }, { once: true });
34219
+ let onAbort = void 0;
34220
+ const abortPromise = new Promise((_, reject) => {
34221
+ onAbort = () => reject(createAbortError());
34222
+ signal.addEventListener("abort", onAbort, { once: true });
34215
34223
  });
34224
+ try {
34225
+ return await Promise.race([work, abortPromise]);
34226
+ } finally {
34227
+ if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
34228
+ }
34216
34229
  }
34217
34230
  function messageToGoogleGenAI(message) {
34218
34231
  if (message.role === "tool") throw new ChatProviderError("Tool messages must be converted via messagesToGoogleGenAIContents.");
@@ -34582,8 +34595,8 @@ var GoogleGenAIChatProvider = class {
34582
34595
  contents,
34583
34596
  config
34584
34597
  };
34585
- if (this._stream) return new GoogleGenAIStreamedMessage(await Promise.race([models.generateContentStream(params), abortPromise(options?.signal)]), true, options?.signal);
34586
- return new GoogleGenAIStreamedMessage(await Promise.race([models.generateContent(params), abortPromise(options?.signal)]), false, options?.signal);
34598
+ if (this._stream) return new GoogleGenAIStreamedMessage(await abortRace(options?.signal, models.generateContentStream(params)), true, options?.signal);
34599
+ return new GoogleGenAIStreamedMessage(await abortRace(options?.signal, models.generateContent(params)), false, options?.signal);
34587
34600
  } catch (error) {
34588
34601
  if (error instanceof DOMException && error.name === "AbortError") throw error;
34589
34602
  throw convertGoogleGenAIError(error);
@@ -53403,6 +53416,17 @@ var MemoryMemoStore = class MemoryMemoStore {
53403
53416
  async append(entry) {
53404
53417
  return this.withWriteLock(() => this.appendInternal(entry));
53405
53418
  }
53419
+ /**
53420
+ * True when a memo with the same source session + user need + approach
53421
+ * already exists. Used by compaction-summary recovery to re-store memos
53422
+ * idempotently after a crash between compaction apply and extraction.
53423
+ * The approach field keeps two distinct memos that happen to share the
53424
+ * same user need from being collapsed into one.
53425
+ */
53426
+ existsBySourceAndNeed(sourceSessionId, userNeed, approach) {
53427
+ if (this.db === void 0) return false;
53428
+ return this.db.prepare("SELECT 1 FROM memos WHERE source_session_id = ? AND user_need = ? AND approach = ? LIMIT 1").get(sourceSessionId, userNeed, approach) !== void 0;
53429
+ }
53406
53430
  /** Delete a memo by id. */
53407
53431
  async delete(id) {
53408
53432
  return this.withWriteLock(() => this.deleteInternal(id));
@@ -79164,6 +79188,31 @@ function extractPreviousSummary(history) {
79164
79188
  const text = head.content.filter((p) => p.type === "text").map((p) => p.text).join("");
79165
79189
  return text.length > 0 ? text : null;
79166
79190
  }
79191
+ /**
79192
+ * Crash-recovery path for compaction memos. `context.apply_compaction` is
79193
+ * written to wire before the extraction step runs; if the process dies in
79194
+ * that window the memos are lost forever (nothing re-extracts them). On wire
79195
+ * replay the summary is available again, so re-parse it and store anything
79196
+ * missing. Idempotent: a healthy replay finds the memos already stored
79197
+ * (existsBySourceAndNeed) and skips them, so recovery never duplicates.
79198
+ */
79199
+ async function recoverMemosFromCompactionSummary(agent, summary) {
79200
+ const memoStore = agent.memoStore;
79201
+ if (!memoStore || summary === void 0 || summary.trim().length === 0) return;
79202
+ const memos = parseMemoryMemos(summary);
79203
+ if (memos.length === 0) return;
79204
+ const sessionId = agent.homedir ? basename$1(dirname$2(dirname$2(agent.homedir))) : "unknown";
79205
+ const sessionTitle = await agent.getSessionTitle().catch(() => void 0);
79206
+ const projectDir = agent.config.cwd;
79207
+ const failed = (await Promise.allSettled(memos.map(async (memo) => {
79208
+ if (memoStore.existsBySourceAndNeed(sessionId, memo.userNeed, memo.approach)) return;
79209
+ memo.sourceSessionId = sessionId;
79210
+ memo.sourceSessionTitle = sessionTitle ?? "";
79211
+ memo.projectDir = projectDir;
79212
+ await memoStore.append(memo);
79213
+ }))).filter((result) => result.status === "rejected").length;
79214
+ if (failed > 0) agent.log.warn("Some recovered memory memos failed to store", { failed });
79215
+ }
79167
79216
  //#endregion
79168
79217
  //#region ../../packages/agent-core/src/flags/registry.ts
79169
79218
  /**
@@ -84844,6 +84893,7 @@ var FileSystemAgentRecordPersistence = class {
84844
84893
  pendingRecords = [];
84845
84894
  shouldClear = false;
84846
84895
  directorySynced = false;
84896
+ rewriteSeq = 0;
84847
84897
  flushPromise;
84848
84898
  error;
84849
84899
  constructor(filePath, options = {}) {
@@ -84953,7 +85003,21 @@ var FileSystemAgentRecordPersistence = class {
84953
85003
  const content = (this.options.blobStore !== void 0 ? await Promise.all(batch.map((record) => this.options.blobStore.offload(record))) : batch).map((e) => JSON.stringify(e) + "\n").join("");
84954
85004
  const directory = dirname$2(this.filePath);
84955
85005
  await mkdir(directory, { recursive: true });
84956
- const fh = await open(this.filePath, shouldClear ? "w" : "a");
85006
+ if (shouldClear) {
85007
+ const tmpPath = `${this.filePath}.${process.pid}.${this.rewriteSeq++}.tmp`;
85008
+ const tmp = await open(tmpPath, "w");
85009
+ try {
85010
+ if (content.length > 0) await tmp.writeFile(content, "utf8");
85011
+ await tmp.sync();
85012
+ } finally {
85013
+ await tmp.close();
85014
+ }
85015
+ await rename(tmpPath, this.filePath);
85016
+ await syncDir(directory);
85017
+ this.directorySynced = true;
85018
+ return;
85019
+ }
85020
+ const fh = await open(this.filePath, "a");
84957
85021
  try {
84958
85022
  if (content.length > 0) await fh.writeFile(content, "utf8");
84959
85023
  await fh.sync();
@@ -85310,6 +85374,7 @@ function restoreAgentRecord(agent, input) {
85310
85374
  return;
85311
85375
  case "context.apply_compaction":
85312
85376
  agent.context.applyCompaction(input);
85377
+ recoverMemosFromCompactionSummary(agent, input.summary);
85313
85378
  return;
85314
85379
  case "context.snapshot":
85315
85380
  agent.context.restoreJSONSnapshot(input.snapshot);
@@ -85406,11 +85471,19 @@ var AgentRecords = class {
85406
85471
  snapshotIndex = i;
85407
85472
  break;
85408
85473
  }
85474
+ let foldedCompactionSummary;
85409
85475
  for (let i = 0; i < replayedRecords.length; i++) {
85410
85476
  const record = replayedRecords[i];
85411
85477
  if (!record) continue;
85412
- if (i < snapshotIndex && isSnapshotFoldedContextRecord(record.type)) continue;
85478
+ if (i < snapshotIndex && isSnapshotFoldedContextRecord(record.type)) {
85479
+ if (record.type === "context.apply_compaction") foldedCompactionSummary = record.summary;
85480
+ continue;
85481
+ }
85413
85482
  this.restore(record);
85483
+ if (record.type === "context.snapshot" && foldedCompactionSummary !== void 0) {
85484
+ recoverMemosFromCompactionSummary(this.agent, foldedCompactionSummary);
85485
+ foldedCompactionSummary = void 0;
85486
+ }
85414
85487
  }
85415
85488
  this.agent.context.dropVacuousOpenMessages();
85416
85489
  if (shouldRewrite) {
@@ -105566,6 +105639,9 @@ var SessionSubagentHost = class {
105566
105639
  backgroundTaskTimeoutMs;
105567
105640
  modelBindings;
105568
105641
  activeChildren = /* @__PURE__ */ new Map();
105642
+ /** Per-child per-model usage already folded into the parent totals, so a
105643
+ * resumed child's aggregation only adds the delta. */
105644
+ aggregatedChildUsage = /* @__PURE__ */ new WeakMap();
105569
105645
  constructor(session, ownerAgentId, backgroundTaskTimeoutMs, modelBindings) {
105570
105646
  this.session = session;
105571
105647
  this.ownerAgentId = ownerAgentId;
@@ -105709,6 +105785,21 @@ var SessionSubagentHost = class {
105709
105785
  result = lastAssistantText$1(child);
105710
105786
  }
105711
105787
  const usage = child.usage.data().total;
105788
+ const childByModel = child.usage.data().byModel ?? {};
105789
+ const previous = this.aggregatedChildUsage.get(child) ?? {};
105790
+ for (const [model, childUsage] of Object.entries(childByModel)) {
105791
+ const delta = previous[model] === void 0 ? childUsage : subtractUsage(childUsage, previous[model]);
105792
+ if (isZeroUsage(delta)) continue;
105793
+ try {
105794
+ parent.usage.record(model, delta, "session");
105795
+ } catch (error) {
105796
+ parent.log.warn("Failed to aggregate subagent usage", {
105797
+ model,
105798
+ error: String(error)
105799
+ });
105800
+ }
105801
+ }
105802
+ this.aggregatedChildUsage.set(child, childByModel);
105712
105803
  let findingsBlock = "";
105713
105804
  if (profileName === "reviewer") {
105714
105805
  const findings = getFindingsFromStore(child.tools.toolStore);
@@ -105813,6 +105904,18 @@ var SessionSubagentHost = class {
105813
105904
  });
105814
105905
  }
105815
105906
  };
105907
+ /** Element-wise subtraction clamped at zero (usage can never go negative). */
105908
+ function subtractUsage(current, previous) {
105909
+ return {
105910
+ inputOther: Math.max(0, current.inputOther - previous.inputOther),
105911
+ output: Math.max(0, current.output - previous.output),
105912
+ inputCacheRead: Math.max(0, current.inputCacheRead - previous.inputCacheRead),
105913
+ inputCacheCreation: Math.max(0, current.inputCacheCreation - previous.inputCacheCreation)
105914
+ };
105915
+ }
105916
+ function isZeroUsage(usage) {
105917
+ return usage.inputOther === 0 && usage.output === 0 && usage.inputCacheRead === 0 && usage.inputCacheCreation === 0;
105918
+ }
105816
105919
  async function runChildTurnToCompletion(child, signal) {
105817
105920
  const completion = await child.turn.waitForCurrentTurn(signal);
105818
105921
  const turnEnded = completion.event;
@@ -123049,28 +123152,24 @@ var ScreamCore = class {
123049
123152
  return this.config;
123050
123153
  }
123051
123154
  async setScreamConfig(input) {
123052
- return this.enqueueConfigMutation(async () => {
123053
- const config = mergeConfigPatch(readConfigFile(this.configPath), input);
123054
- await writeConfigFile(this.configPath, config);
123055
- return this.config = loadRuntimeConfig(this.configPath);
123056
- });
123155
+ const config = mergeConfigPatch(readConfigFile(this.configPath), input);
123156
+ await writeConfigFile(this.configPath, config);
123157
+ return this.config = loadRuntimeConfig(this.configPath);
123057
123158
  }
123058
123159
  async removeScreamProvider(input) {
123059
- return this.enqueueConfigMutation(async () => {
123060
- const config = readConfigFile(this.configPath);
123061
- delete config.providers[input.providerId];
123062
- let removedDefault = false;
123063
- const existingModels = config.models ?? {};
123064
- for (const [key, model] of Object.entries(existingModels)) if (typeof model === "object" && model !== null && !Array.isArray(model) && model["provider"] === input.providerId) {
123065
- delete existingModels[key];
123066
- if (config.defaultModel === key) removedDefault = true;
123067
- }
123068
- config.models = existingModels;
123069
- if (removedDefault) config.defaultModel = void 0;
123070
- if (config.defaultProvider === input.providerId) config.defaultProvider = void 0;
123071
- await writeConfigFile(this.configPath, config);
123072
- return this.config = loadRuntimeConfig(this.configPath);
123073
- });
123160
+ const config = readConfigFile(this.configPath);
123161
+ delete config.providers[input.providerId];
123162
+ let removedDefault = false;
123163
+ const existingModels = config.models ?? {};
123164
+ for (const [key, model] of Object.entries(existingModels)) if (typeof model === "object" && model !== null && !Array.isArray(model) && model["provider"] === input.providerId) {
123165
+ delete existingModels[key];
123166
+ if (config.defaultModel === key) removedDefault = true;
123167
+ }
123168
+ config.models = existingModels;
123169
+ if (removedDefault) config.defaultModel = void 0;
123170
+ if (config.defaultProvider === input.providerId) config.defaultProvider = void 0;
123171
+ await writeConfigFile(this.configPath, config);
123172
+ return this.config = loadRuntimeConfig(this.configPath);
123074
123173
  }
123075
123174
  setRuntimeSystemPrompt({ sessionId, ...payload }) {
123076
123175
  return this.sessionApi(sessionId).setRuntimeSystemPrompt(payload);
@@ -123612,23 +123711,6 @@ var ScreamCore = class {
123612
123711
  reloadProviderManager() {
123613
123712
  return this.config = loadRuntimeConfig(this.configPath);
123614
123713
  }
123615
- /**
123616
- * Serialize config-file mutations. Both setScreamConfig and
123617
- * removeScreamProvider are read-modify-write cycles that read the file
123618
- * synchronously and then await the write; two calls issued together (e.g.
123619
- * cycling thinking levels plus confirming a model in the picker fire
123620
- * back-to-back setConfig patches) would otherwise read the SAME stale
123621
- * snapshot, and the later write silently reverts the earlier patch — a
123622
- * lost update that makes e.g. a just-selected default model flip back to
123623
- * the old one. The queue guarantees each cycle re-reads after the
123624
- * previous write completed.
123625
- */
123626
- configMutationQueue = Promise.resolve();
123627
- enqueueConfigMutation(mutate) {
123628
- const run = this.configMutationQueue.then(mutate, mutate);
123629
- this.configMutationQueue = run.catch(() => void 0);
123630
- return run;
123631
- }
123632
123714
  async refreshSessionRuntimeConfig(session, config) {
123633
123715
  const api = new SessionAPIImpl(session);
123634
123716
  const requested = (await api.getModel({ agentId: "main" })).trim();
@@ -130817,10 +130899,11 @@ async function changeThinkingLevel(host, alias, level) {
130817
130899
  }
130818
130900
  const prevLevel = host.state.appState.thinkingLevel;
130819
130901
  const isActiveModel = alias === host.state.appState.model;
130820
- const session = host.session;
130821
130902
  if (isActiveModel && level !== prevLevel) {
130903
+ const session = host.session;
130822
130904
  try {
130823
- if (session !== void 0) await session.setThinking(level);
130905
+ if (session === void 0) await host.authFlow.activateModelAfterLogin(alias, level);
130906
+ else await session.setThinking(level);
130824
130907
  } catch (error) {
130825
130908
  const msg = formatErrorMessage(error);
130826
130909
  host.showError(`Failed to set thinking: ${msg}`);
@@ -130836,7 +130919,7 @@ async function changeThinkingLevel(host, alias, level) {
130836
130919
  host.showError(`Failed to save thinking default: ${msg}`);
130837
130920
  return;
130838
130921
  }
130839
- const status = isActiveModel && level !== prevLevel ? session === void 0 ? persisted ? `No active session - thinking ${level} for ${alias} saved as default.` : `No active session - thinking ${level} was NOT saved (${alias} is not the default model, so its level applies only when a new session selects it).` : `Thinking set to ${level} for ${alias}.` : persisted ? `Saved thinking ${level} as default for ${alias}.` : `Thinking already ${level} for ${alias}.`;
130922
+ const status = isActiveModel && level !== prevLevel ? `Thinking set to ${level} for ${alias}.` : persisted ? `Saved thinking ${level} as default for ${alias}.` : `Thinking already ${level} for ${alias}.`;
130840
130923
  host.showStatus(status, host.state.theme.colors.success);
130841
130924
  }
130842
130925
  async function persistThinkingDefault(host, alias, level) {
@@ -130865,16 +130948,18 @@ async function performModelSwitch(host, alias, thinkingLevel) {
130865
130948
  const prevThinkingLevel = host.state.appState.thinkingLevel;
130866
130949
  const modelChanged = alias !== prevModel;
130867
130950
  const thinkingChanged = thinkingLevel !== prevThinkingLevel;
130868
- const session = host.session;
130869
- const overflow = session !== void 0 && alias !== prevModel ? contextOverflowForModel(host.state.appState, alias) : null;
130951
+ const needsSessionActivation = modelChanged || thinkingChanged;
130952
+ const overflow = alias !== prevModel ? contextOverflowForModel(host.state.appState, alias) : null;
130870
130953
  if (overflow !== null) {
130871
130954
  host.showNotice("Storm Breaker(风暴守护者)", `无法切换到模型「${alias}」:当前会话上下文 ${formatTokenCount$1(overflow.currentTokens)} 已超出该模型上限 ${formatTokenCount$1(overflow.maxContextTokens)}。建议先执行 /compact 压缩上下文,或选择上下文窗口更大的模型。`);
130872
130955
  return;
130873
130956
  }
130957
+ const session = host.session;
130874
130958
  let effectiveAlias = alias;
130875
130959
  let effectiveThinking = thinkingLevel;
130876
130960
  try {
130877
- if (session !== void 0) {
130961
+ if (session === void 0 && needsSessionActivation) await host.authFlow.activateModelAfterLogin(alias, thinkingLevel);
130962
+ else if (session !== void 0) {
130878
130963
  if (modelChanged) await session.setModel(alias);
130879
130964
  if (thinkingChanged) await session.setThinking(thinkingLevel);
130880
130965
  const confirmed = await session.getStatus().catch(() => null);
@@ -130897,13 +130982,12 @@ async function performModelSwitch(host, alias, thinkingLevel) {
130897
130982
  persisted = await persistModelSelection(host, alias, thinkingLevel);
130898
130983
  } catch (error) {
130899
130984
  const msg = formatErrorMessage(error);
130900
- host.showError(`${session === void 0 ? "Selected" : "Switched to"} ${effectiveAlias}, but failed to save default: ${msg}`);
130985
+ host.showError(`Switched to ${effectiveAlias}, but failed to save default: ${msg}`);
130901
130986
  return;
130902
130987
  }
130903
130988
  const hasHistory = host.state.appState.contextTokens > 0;
130904
130989
  const cacheWarning = modelChanged && hasHistory ? " Note: switching models invalidates the existing prompt cache - use /new to avoid extra token costs." : "";
130905
130990
  const status = (() => {
130906
- if (session === void 0 && (modelChanged || thinkingChanged)) return `No active session - ${effectiveAlias} (thinking ${effectiveThinking}) saved as default. It applies when you start a new session.`;
130907
130991
  if (modelChanged) return `Switched to ${effectiveAlias} with thinking ${effectiveThinking}.${cacheWarning}`;
130908
130992
  if (thinkingChanged) return `Thinking set to ${effectiveThinking} for ${effectiveAlias}.`;
130909
130993
  if (persisted) return `Saved ${effectiveAlias} with thinking ${effectiveThinking} as default.`;
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
3
  import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
- import { Kt as handleConnectCommand } from "./dispatch-ATQMQq1p.mjs";
6
+ import { Kt as handleConnectCommand } from "./dispatch-B_ViYZer.mjs";
7
7
  export { handleConnectCommand };
package/dist/main.mjs CHANGED
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
7
  //#region src/main.ts
8
8
  try {
9
- (await import("./app-C3aR5fdI.mjs")).main();
9
+ (await import("./app-6Hcxs2tP.mjs")).main();
10
10
  } catch (error) {
11
11
  process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
12
12
  process.exit(1);