@p4code/cli 0.1.23 → 0.1.25

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/dist/bin.mjs CHANGED
@@ -236,7 +236,7 @@ const make$76 = () => {
236
236
  const layer$72 = Layer.sync(NetService, make$76);
237
237
  //#endregion
238
238
  //#region package.json
239
- var version = "0.1.23";
239
+ var version = "0.1.25";
240
240
  //#endregion
241
241
  //#region src/config.ts
242
242
  /**
@@ -5035,6 +5035,13 @@ const ThreadTokenUsageSnapshot = Schema$1.Struct({
5035
5035
  toolUses: Schema$1.optional(NonNegativeInt),
5036
5036
  durationMs: Schema$1.optional(NonNegativeInt),
5037
5037
  compactsAutomatically: Schema$1.optional(Schema$1.Boolean),
5038
+ /**
5039
+ * The compress mode the turn these deltas belong to actually ran under, not
5040
+ * the thread's mode now. Without it the per-turn output figures cannot be
5041
+ * attributed, and the whole question of what compression costs stays a guess.
5042
+ * Optional because snapshots recorded before it existed have no answer.
5043
+ */
5044
+ compressMode: Schema$1.optional(CompressMode),
5038
5045
  breakdown: Schema$1.optional(ContextWindowBreakdown)
5039
5046
  });
5040
5047
  const ThreadTokenUsageUpdatedPayload = Schema$1.Struct({ usage: ThreadTokenUsageSnapshot });
@@ -16151,6 +16158,36 @@ function buildSkillOverrides(disabled, discovered) {
16151
16158
  * @module provider/Drivers/ClaudeSkills
16152
16159
  */
16153
16160
  const FRONTMATTER_PATTERN$1 = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
16161
+ /** The `true` spellings the CLI accepts for a boolean frontmatter field. */
16162
+ const TRUTHY_FRONTMATTER_VALUES = /* @__PURE__ */ new Set([
16163
+ "true",
16164
+ "yes",
16165
+ "on",
16166
+ "1"
16167
+ ]);
16168
+ /**
16169
+ * A frontmatter boolean, spelled any of the ways the CLI allows.
16170
+ *
16171
+ * YAML already decodes an unquoted `true` to a boolean, but a quoted `"yes"`
16172
+ * arrives as a string, and the CLI accepts both. Reading only the boolean would
16173
+ * make a skill that works there silently model-invocable here.
16174
+ */
16175
+ function readFrontmatterBoolean(value) {
16176
+ if (typeof value === "boolean") return value;
16177
+ if (typeof value === "number") return value === 1;
16178
+ if (typeof value === "string") return TRUTHY_FRONTMATTER_VALUES.has(value.trim().toLowerCase());
16179
+ }
16180
+ /**
16181
+ * The document with its frontmatter block removed.
16182
+ *
16183
+ * The body is what a skill actually instructs; the header is metadata about
16184
+ * how it is selected. Exported for the expansion that inlines a skill into a
16185
+ * turn, which must send the instructions without the routing header.
16186
+ */
16187
+ function stripMarkdownFrontmatter(contents) {
16188
+ const match = FRONTMATTER_PATTERN$1.exec(contents);
16189
+ return (match ? contents.slice(match[0].length) : contents).trim();
16190
+ }
16154
16191
  /**
16155
16192
  * The `name`/`description` header a skill and a subagent definition both carry.
16156
16193
  *
@@ -16172,10 +16209,12 @@ function parseMarkdownFrontmatter(contents) {
16172
16209
  const record = parsed;
16173
16210
  const name = typeof record.name === "string" ? record.name.trim() : "";
16174
16211
  const description = typeof record.description === "string" ? record.description.trim() : "";
16212
+ const disableModelInvocation = readFrontmatterBoolean(record["disable-model-invocation"]);
16175
16213
  return {
16176
16214
  kind: "parsed",
16177
16215
  ...name ? { name } : {},
16178
- ...description ? { description } : {}
16216
+ ...description ? { description } : {},
16217
+ ...disableModelInvocation === void 0 ? {} : { disableModelInvocation }
16179
16218
  };
16180
16219
  }
16181
16220
  /**
@@ -18928,7 +18967,7 @@ const COMPRESS_SHARED_RULES = `Respond terse. All technical substance stays. Onl
18928
18967
 
18929
18968
  ## Persistence
18930
18969
 
18931
- Active every response. No revert after many turns. No filler drift. Still active if unsure.
18970
+ Active every response. No filler drift, no drifting back to verbose prose on your own. A later instruction that switches the response style off or changes its intensity replaces this block: obey the most recent one, not this one.
18932
18971
 
18933
18972
  ## Rules
18934
18973
 
@@ -19016,12 +19055,35 @@ const COMPRESS_TURN_REMINDERS = {
19016
19055
  full: "[response style: compressed full - terse fragments, drop articles/filler; keep negations and numbers exact; code/commits/security text normal]",
19017
19056
  ultra: "[response style: compressed ultra - maximum terseness, one word when enough; keep negations and numbers exact; code/commits/security text normal]"
19018
19057
  };
19058
+ /**
19059
+ * Sent when compression is switched off on a session that is still carrying a
19060
+ * ruleset it cannot un-send. Silence is not enough: the ruleset states its own
19061
+ * persistence, so the absence of a reminder reads to the model as "nothing
19062
+ * changed" rather than "stop".
19063
+ */
19064
+ const COMPRESS_REVERT_REMINDER = "[response style: compression off - any earlier response-compression instruction in this session no longer applies; write normal, complete prose from here on]";
19019
19065
  function compressRulesetFor(mode) {
19020
19066
  return mode === "off" ? void 0 : COMPRESS_RULESETS[mode];
19021
19067
  }
19022
19068
  function compressTurnReminderFor(mode) {
19023
19069
  return mode === "off" ? void 0 : COMPRESS_TURN_REMINDERS[mode];
19024
19070
  }
19071
+ /**
19072
+ * What to prepend to a turn's message so the running session ends up in the
19073
+ * requested compress mode.
19074
+ *
19075
+ * `staleRulesetMode` is the mode whose ruleset the session is still carrying in
19076
+ * a channel that cannot be rebuilt for this turn - Claude's system prompt is
19077
+ * frozen at session start, and a first-turn message prefix lives in the
19078
+ * conversation history forever. Codex rebuilds its developer instructions every
19079
+ * turn, so callers pass `undefined` for it and no revert line is spent.
19080
+ */
19081
+ function compressTurnPrefixFor(input) {
19082
+ if (input.injectRuleset) return compressRulesetFor(input.mode);
19083
+ const reminder = compressTurnReminderFor(input.mode);
19084
+ if (reminder !== void 0) return reminder;
19085
+ return input.staleRulesetMode !== void 0 && input.staleRulesetMode !== "off" ? COMPRESS_REVERT_REMINDER : void 0;
19086
+ }
19025
19087
  //#endregion
19026
19088
  //#region src/sync/agentPresets.ts
19027
19089
  /**
@@ -88365,6 +88427,8 @@ const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 1e4;
88365
88427
  const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120);
88366
88428
  const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 1e4;
88367
88429
  const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120);
88430
+ const COMPRESS_MODE_BY_THREAD_CACHE_CAPACITY = 1e4;
88431
+ const COMPRESS_MODE_BY_THREAD_TTL = Duration.minutes(120);
88368
88432
  const MAX_BUFFERED_ASSISTANT_CHARS = 24e3;
88369
88433
  const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.P4CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";
88370
88434
  function toTurnId$1(value) {
@@ -88437,9 +88501,12 @@ function assistantSegmentBaseKeyFromEvent(event) {
88437
88501
  function assistantSegmentMessageId(baseKey, segmentIndex) {
88438
88502
  return MessageId.make(segmentIndex === 0 ? `assistant:${baseKey}` : `assistant:${baseKey}:segment:${segmentIndex}`);
88439
88503
  }
88440
- function buildContextWindowActivityPayload(event) {
88504
+ function buildContextWindowActivityPayload(event, compressMode) {
88441
88505
  if (event.type !== "thread.token-usage.updated" || event.payload.usage.usedTokens <= 0) return;
88442
- return event.payload.usage;
88506
+ return compressMode === void 0 ? event.payload.usage : {
88507
+ ...event.payload.usage,
88508
+ compressMode
88509
+ };
88443
88510
  }
88444
88511
  function normalizeRuntimeTurnState(value) {
88445
88512
  switch (value) {
@@ -88474,7 +88541,7 @@ function requestKindFromCanonicalRequestType(requestType) {
88474
88541
  default: return;
88475
88542
  }
88476
88543
  }
88477
- function runtimeEventToActivities(event, taskTitle) {
88544
+ function runtimeEventToActivities(event, taskTitle, compressMode) {
88478
88545
  const maybeSequence = (() => {
88479
88546
  const eventWithSequence = event;
88480
88547
  return eventWithSequence.sessionSequence !== void 0 ? { sequence: eventWithSequence.sessionSequence } : {};
@@ -88684,7 +88751,7 @@ function runtimeEventToActivities(event, taskTitle) {
88684
88751
  ...maybeSequence
88685
88752
  }];
88686
88753
  case "thread.token-usage.updated": {
88687
- const payload = buildContextWindowActivityPayload(event);
88754
+ const payload = buildContextWindowActivityPayload(event, compressMode);
88688
88755
  if (!payload) return [];
88689
88756
  return [{
88690
88757
  id: event.eventId,
@@ -88785,6 +88852,11 @@ const make$3 = Effect.gen(function* () {
88785
88852
  timeToLive: TASK_DESCRIPTION_BY_TASK_TTL,
88786
88853
  lookup: () => Effect.succeed("")
88787
88854
  });
88855
+ const compressModeByThreadId = yield* Cache.make({
88856
+ capacity: COMPRESS_MODE_BY_THREAD_CACHE_CAPACITY,
88857
+ timeToLive: COMPRESS_MODE_BY_THREAD_TTL,
88858
+ lookup: () => Effect.die(/* @__PURE__ */ new Error("compress mode should be read through getOption"))
88859
+ });
88788
88860
  const rememberTaskDescription = (threadId, taskId, description) => Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description);
88789
88861
  const lookupTaskDescription = (threadId, taskId) => Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe(Effect.map((description) => Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined)));
88790
88862
  const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId) {
@@ -89291,7 +89363,9 @@ const make$3 = Effect.gen(function* () {
89291
89363
  taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
89292
89364
  if (!taskTitle) taskTitle = findTaskTitleInActivities((yield* getLoadedThreadDetail())?.activities, event.payload.taskId);
89293
89365
  }
89294
- const activities = runtimeEventToActivities(event, taskTitle);
89366
+ if (event.type === "turn.started") yield* Cache.set(compressModeByThreadId, thread.id, thread.compressMode);
89367
+ const turnCompressMode = event.type === "thread.token-usage.updated" ? (yield* Cache.getOption(compressModeByThreadId, thread.id).pipe(Effect.map(Option.getOrUndefined))) ?? thread.compressMode : void 0;
89368
+ const activities = runtimeEventToActivities(event, taskTitle, turnCompressMode);
89295
89369
  yield* Effect.forEach(activities, (activity) => providerCommandId(event, "thread-activity-append").pipe(Effect.flatMap((commandId) => orchestrationEngine.dispatch({
89296
89370
  type: "thread.activity.append",
89297
89371
  commandId,
@@ -89332,6 +89406,126 @@ const make$3 = Effect.gen(function* () {
89332
89406
  });
89333
89407
  const ProviderRuntimeIngestionLive = Layer.effect(ProviderRuntimeIngestionService, make$3).pipe(Layer.provide(ProjectionTurnRepositoryLive));
89334
89408
  //#endregion
89409
+ //#region src/provider/userInvokedSkills.ts
89410
+ /**
89411
+ * Running a skill the model is not allowed to reach.
89412
+ *
89413
+ * A skill carrying `disable-model-invocation: true` is withheld from the model
89414
+ * entirely — description included — so only a person typing `/name` can start
89415
+ * it. That works in the interactive CLI, which expands the slash command before
89416
+ * the model ever sees it. It does not work here: p4code drives the Agent SDK,
89417
+ * whose non-interactive path forwards the typed text verbatim, and p4code's own
89418
+ * fallback is to ask the model to call the `Skill` tool — which is the one door
89419
+ * this flag locks. The result is a skill that is unreachable from both sides,
89420
+ * with nothing in the transcript to say why.
89421
+ *
89422
+ * So the expansion happens here, server-side, before the turn leaves: the
89423
+ * skill's own instructions are inlined into the message the provider receives.
89424
+ * The transcript keeps what the person typed, the same split compression
89425
+ * already uses.
89426
+ *
89427
+ * **Only skills the model may not invoke are expanded.** Everything else is
89428
+ * left alone deliberately — those already work through the `Skill` tool, and
89429
+ * inlining them too would mean two mechanisms racing to run the same skill.
89430
+ *
89431
+ * @module provider/userInvokedSkills
89432
+ */
89433
+ /**
89434
+ * A leading `/name`, with whatever followed it.
89435
+ *
89436
+ * Anchored at the start: a slash somewhere inside a sentence is a path, a date
89437
+ * or an "and/or", and treating those as commands would rewrite ordinary
89438
+ * messages. The name pattern matches what a skill directory can be called,
89439
+ * including the `plugin:skill` form.
89440
+ */
89441
+ const SLASH_INVOCATION = /^\/([A-Za-z0-9][A-Za-z0-9_:-]*)[ \t]*([\s\S]*)$/;
89442
+ /** `SKILL.md` is the only file a skill is required to have. */
89443
+ const SKILL_FILENAME = "SKILL.md";
89444
+ const parseSlashInvocation = (text) => {
89445
+ const match = SLASH_INVOCATION.exec(text.trim());
89446
+ const name = match?.[1];
89447
+ if (name === void 0) return void 0;
89448
+ return {
89449
+ name,
89450
+ args: (match?.[2] ?? "").trim()
89451
+ };
89452
+ };
89453
+ /**
89454
+ * The message the provider receives in place of `/name args`.
89455
+ *
89456
+ * Tagged rather than pasted bare so the instructions cannot be mistaken for the
89457
+ * person's own words, and attributed to its file so an agent that needs the
89458
+ * skill's siblings — a reference, a template — knows where to look.
89459
+ */
89460
+ const buildUserInvokedSkillInput = (input) => [
89461
+ `<skill name="${input.name}" path="${input.path}">`,
89462
+ input.body,
89463
+ "</skill>",
89464
+ "",
89465
+ `The user invoked the ${input.name} skill${input.args ? ` with: ${input.args}` : ""}. Follow the instructions above.`
89466
+ ].join("\n");
89467
+ /**
89468
+ * The `SKILL.md` a typed name refers to, searched across the given roots.
89469
+ *
89470
+ * Both names a skill answers to are matched — the frontmatter `name` and the
89471
+ * directory it lives in — because either is what a person sees depending on the
89472
+ * surface they read it from, and matching one only would leave a skill whose
89473
+ * two names disagree quietly unreachable. Later roots win, which is how the
89474
+ * CLI resolves a project skill over a user one.
89475
+ */
89476
+ const findSkillFile = Effect.fnUntraced(function* (input) {
89477
+ const fileSystem = yield* FileSystem.FileSystem;
89478
+ const path = yield* Path.Path;
89479
+ let found;
89480
+ for (const root of input.roots) {
89481
+ const entries = yield* fileSystem.readDirectory(root).pipe(Effect.orElseSucceed(() => []));
89482
+ for (const entry of [...entries].sort()) {
89483
+ const skillPath = path.join(root, entry, SKILL_FILENAME);
89484
+ const contents = yield* fileSystem.readFileString(skillPath).pipe(Effect.orElseSucceed(() => void 0));
89485
+ if (contents === void 0) continue;
89486
+ const frontmatter = parseMarkdownFrontmatter(contents);
89487
+ const frontmatterName = frontmatter.kind === "parsed" ? frontmatter.name : void 0;
89488
+ if (entry !== input.name && frontmatterName !== input.name) continue;
89489
+ found = {
89490
+ path: skillPath,
89491
+ contents
89492
+ };
89493
+ }
89494
+ }
89495
+ return found;
89496
+ });
89497
+ /**
89498
+ * The turn's text, with a user-invoked skill inlined when there is one.
89499
+ *
89500
+ * `undefined` means "nothing to do", which covers every ordinary message: no
89501
+ * leading slash, a name no skill answers to, a skill the model can invoke for
89502
+ * itself, or a body that is empty once the frontmatter comes off. Reading the
89503
+ * filesystem is best-effort throughout — an unreadable root or file leaves the
89504
+ * message exactly as it was typed, because failing a turn over a skill lookup
89505
+ * would be a worse answer than sending the text.
89506
+ *
89507
+ * @param roots - Skill directories, least specific first.
89508
+ */
89509
+ const expandUserInvokedSkill = Effect.fnUntraced(function* (input) {
89510
+ const invocation = parseSlashInvocation(input.text);
89511
+ if (invocation === void 0) return void 0;
89512
+ const skill = yield* findSkillFile({
89513
+ name: invocation.name,
89514
+ roots: input.roots
89515
+ });
89516
+ if (skill === void 0) return void 0;
89517
+ const frontmatter = parseMarkdownFrontmatter(skill.contents);
89518
+ if (frontmatter.kind !== "parsed" || frontmatter.disableModelInvocation !== true) return;
89519
+ const body = stripMarkdownFrontmatter(skill.contents);
89520
+ if (body.length === 0) return void 0;
89521
+ return buildUserInvokedSkillInput({
89522
+ name: invocation.name,
89523
+ path: skill.path,
89524
+ body,
89525
+ args: invocation.args
89526
+ });
89527
+ });
89528
+ //#endregion
89335
89529
  //#region src/orchestration/Layers/ProviderCommandReactor.ts
89336
89530
  const isProviderAdapterRequestError = Schema$1.is(ProviderAdapterRequestError);
89337
89531
  const isProviderDriverKind = Schema$1.is(ProviderDriverKind);
@@ -89415,6 +89609,15 @@ const make$2 = Effect.gen(function* () {
89415
89609
  });
89416
89610
  const hasHandledTurnStartRecently = (key) => Cache.getOption(handledTurnStartKeys, key).pipe(Effect.flatMap((cached) => Cache.set(handledTurnStartKeys, key, true).pipe(Effect.as(Option.isSome(cached)))));
89417
89611
  const threadModelSelections = /* @__PURE__ */ new Map();
89612
+ /**
89613
+ * The compress mode whose ruleset the thread's live session was handed at
89614
+ * start, kept because two of the three delivery channels cannot be rebuilt
89615
+ * afterwards: Claude bakes the ruleset into the system prompt of a
89616
+ * long-lived process, and the prefix providers put it in the first turn's
89617
+ * message. Toggling compression off has to send a counter-instruction, and
89618
+ * this is how the reactor knows there is something to counter.
89619
+ */
89620
+ const threadSessionRulesetModes = /* @__PURE__ */ new Map();
89418
89621
  const appendProviderFailureActivity = (input) => Effect.all({
89419
89622
  commandId: serverCommandId("provider-failure-activity"),
89420
89623
  eventId: serverEventId()
@@ -89519,6 +89722,25 @@ const make$2 = Effect.gen(function* () {
89519
89722
  const resolveProject = Effect.fnUntraced(function* (projectId) {
89520
89723
  return yield* projectionSnapshotQuery.getProjectShellById(projectId).pipe(Effect.map(Option.getOrUndefined));
89521
89724
  });
89725
+ /**
89726
+ * Where a typed `/name` is looked up: the user's skills, then the
89727
+ * workspace's.
89728
+ *
89729
+ * Project last so it wins, matching the CLI's most-specific-wins resolution.
89730
+ * The user root is resolved with an empty `homePath` on purpose — this runs
89731
+ * before a provider instance is chosen, and the config-dir override that a
89732
+ * single Claude instance may carry is not a fact about the person's skills.
89733
+ */
89734
+ const resolveUserInvokedSkillRoots = Effect.fnUntraced(function* (thread) {
89735
+ const path = yield* Path.Path;
89736
+ const userSkillsDir = yield* resolveClaudeUserSkillsDir({ homePath: "" });
89737
+ const project = yield* resolveProject(thread.projectId);
89738
+ const cwd = resolveThreadWorkspaceCwd({
89739
+ thread,
89740
+ projects: project ? [project] : []
89741
+ });
89742
+ return [userSkillsDir, ...cwd ? [path.join(cwd, ".claude", "skills")] : []];
89743
+ });
89522
89744
  const resolveThread = Effect.fnUntraced(function* (threadId) {
89523
89745
  return yield* projectionSnapshotQuery.getThreadDetailById(threadId).pipe(Effect.map(Option.getOrUndefined));
89524
89746
  });
@@ -89613,17 +89835,20 @@ const make$2 = Effect.gen(function* () {
89613
89835
  thread,
89614
89836
  projects: project ? [project] : []
89615
89837
  });
89616
- const startProviderSession = (input) => providerService.startSession(threadId, {
89617
- threadId,
89618
- ...preferredProvider ? { provider: preferredProvider } : {},
89619
- providerInstanceId: desiredInstanceId,
89620
- ...effectiveCwd ? { cwd: effectiveCwd } : {},
89621
- modelSelection: desiredModelSelection,
89622
- ...input?.resumeCursor !== void 0 ? { resumeCursor: input.resumeCursor } : {},
89623
- runtimeMode: desiredRuntimeMode,
89624
- compressMode: thread.compressMode,
89625
- unpromptedSubagents: thread.unpromptedSubagents
89626
- });
89838
+ const startProviderSession = (input) => {
89839
+ threadSessionRulesetModes.set(threadId, thread.compressMode);
89840
+ return providerService.startSession(threadId, {
89841
+ threadId,
89842
+ ...preferredProvider ? { provider: preferredProvider } : {},
89843
+ providerInstanceId: desiredInstanceId,
89844
+ ...effectiveCwd ? { cwd: effectiveCwd } : {},
89845
+ modelSelection: desiredModelSelection,
89846
+ ...input?.resumeCursor !== void 0 ? { resumeCursor: input.resumeCursor } : {},
89847
+ runtimeMode: desiredRuntimeMode,
89848
+ compressMode: thread.compressMode,
89849
+ unpromptedSubagents: thread.unpromptedSubagents
89850
+ });
89851
+ };
89627
89852
  const bindSessionToThread = (session) => Effect.gen(function* () {
89628
89853
  if (session.providerInstanceId === void 0) return yield* new ProviderAdapterRequestError({
89629
89854
  provider: providerErrorLabel(session.provider),
@@ -89702,6 +89927,10 @@ const make$2 = Effect.gen(function* () {
89702
89927
  });
89703
89928
  if (input.modelSelection !== void 0) threadModelSelections.set(input.threadId, input.modelSelection);
89704
89929
  const normalizedInput = toNonEmptyProviderInput(input.messageText);
89930
+ const expandedInput = normalizedInput === void 0 ? void 0 : (yield* expandUserInvokedSkill({
89931
+ text: normalizedInput,
89932
+ roots: yield* resolveUserInvokedSkillRoots(thread)
89933
+ })) ?? normalizedInput;
89705
89934
  const normalizedAttachments = input.attachments ?? [];
89706
89935
  const activeSession = yield* providerService.listSessions().pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)));
89707
89936
  const sessionModelSwitch = activeSession === void 0 ? "in-session" : activeSession.providerInstanceId === void 0 ? yield* new ProviderAdapterRequestError({
@@ -89716,8 +89945,13 @@ const make$2 = Effect.gen(function* () {
89716
89945
  } : requestedModelSelection : input.modelSelection;
89717
89946
  const compressMode = thread.compressMode;
89718
89947
  const hasSessionLevelRuleset = activeSession?.provider === "claudeAgent" || activeSession?.provider === "codex";
89719
- const compressPrefix = !hadActiveSession && !hasSessionLevelRuleset ? compressRulesetFor(compressMode) : compressTurnReminderFor(compressMode);
89720
- const inputWithCompressPrefix = normalizedInput !== void 0 && compressPrefix !== void 0 ? `${compressPrefix}\n\n${normalizedInput}` : normalizedInput;
89948
+ const rebuildsRulesetEachTurn = activeSession?.provider === "codex";
89949
+ const compressPrefix = compressTurnPrefixFor({
89950
+ mode: compressMode,
89951
+ injectRuleset: !hadActiveSession && !hasSessionLevelRuleset,
89952
+ staleRulesetMode: hadActiveSession && !rebuildsRulesetEachTurn ? threadSessionRulesetModes.get(input.threadId) : void 0
89953
+ });
89954
+ const inputWithCompressPrefix = expandedInput !== void 0 && compressPrefix !== void 0 ? `${compressPrefix}\n\n${expandedInput}` : expandedInput;
89721
89955
  return {
89722
89956
  threadId: input.threadId,
89723
89957
  ...inputWithCompressPrefix ? { input: inputWithCompressPrefix } : {},
@@ -89940,6 +90174,7 @@ const make$2 = Effect.gen(function* () {
89940
90174
  if (!thread) return;
89941
90175
  const now = event.payload.createdAt;
89942
90176
  if (thread.session && thread.session.status !== "stopped") yield* providerService.stopSession({ threadId: thread.id });
90177
+ threadSessionRulesetModes.delete(thread.id);
89943
90178
  yield* setThreadSession({
89944
90179
  threadId: thread.id,
89945
90180
  session: {