@oh-my-pi/pi-coding-agent 16.4.1 → 16.4.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.
@@ -1,7 +1,7 @@
1
1
  import { type TSchema } from "@oh-my-pi/pi-ai";
2
2
  import type { SourceMeta } from "../capability/types.js";
3
3
  import type { CustomTool } from "../extensibility/custom-tools/types.js";
4
- import type { AuthStorage } from "../session/auth-storage.js";
4
+ import { type AuthStorage } from "../session/auth-storage.js";
5
5
  import type { McpConnectionStatusEvent } from "./startup-events.js";
6
6
  import type { MCPToolDetails } from "./tool-bridge.js";
7
7
  import type { MCPToolCache } from "./tool-cache.js";
@@ -125,6 +125,7 @@ export declare class MCPOAuthFlow extends OAuthCallbackFlow {
125
125
  */
126
126
  export interface RefreshMCPOAuthTokenOptions {
127
127
  fetch?: FetchImpl;
128
+ signal?: AbortSignal;
128
129
  /**
129
130
  * Authorization-server URL the original grant was minted against. Used to
130
131
  * filter same-origin resource indicators on refresh. Defaults to `tokenUrl`'s
@@ -97,8 +97,12 @@ export interface BuildSystemPromptOptions {
97
97
  eagerTasks?: boolean;
98
98
  /** When true, the Eager Tasks section uses the hard MUST/ONLY wording (`task.eager: always`) rather than the softer `preferred` nudge. */
99
99
  eagerTasksAlways?: boolean;
100
- /** Whether `task.batch` is enabled; gates batch-call guidance in the Eager Tasks section. */
100
+ /** Whether `task.batch` is enabled; selects the centralized delegation guidance's call shape. */
101
101
  taskBatch?: boolean;
102
+ /** Effective task concurrency limit displayed in centralized delegation guidance. Zero means unlimited. */
103
+ taskMaxConcurrency?: number;
104
+ /** Whether IRC-backed parallel coordination can be included in delegation policy. */
105
+ taskIrcEnabled?: boolean;
102
106
  /** Rules with alwaysApply=true — their full content is injected into the prompt. */
103
107
  alwaysApplyRules?: AlwaysApplyRule[];
104
108
  /** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
@@ -107,8 +111,10 @@ export interface BuildSystemPromptOptions {
107
111
  workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
108
112
  /** Whether the local memory://root summary is active. */
109
113
  memoryRootEnabled?: boolean;
110
- /** Active model identifier (e.g. "anthropic/claude-opus-4") surfaced to the agent. */
114
+ /** Active model identifier (e.g. "anthropic/claude-opus-4") used by prompt policy and optionally surfaced. */
111
115
  model?: string;
116
+ /** Whether to surface `model` in the workstation block. Model-specific prompt policy still uses it. Default: true. */
117
+ includeModelInPrompt?: boolean;
112
118
  /** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */
113
119
  personality?: Personality;
114
120
  /** Whether to include the workspace directory tree in the system prompt. Default: false */
@@ -0,0 +1,2 @@
1
+ /** Whether task guidance should follow Codex's GPT-5.6-specific delegation policy. */
2
+ export declare function usesCodexTaskPrompt(modelId: string | undefined): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.4.1",
4
+ "version": "16.4.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -56,17 +56,17 @@
56
56
  "@agentclientprotocol/sdk": "0.25.0",
57
57
  "@babel/parser": "^7.29.7",
58
58
  "@mozilla/readability": "^0.6.0",
59
- "@oh-my-pi/hashline": "16.4.1",
60
- "@oh-my-pi/omp-stats": "16.4.1",
61
- "@oh-my-pi/pi-agent-core": "16.4.1",
62
- "@oh-my-pi/pi-ai": "16.4.1",
63
- "@oh-my-pi/pi-catalog": "16.4.1",
64
- "@oh-my-pi/pi-mnemopi": "16.4.1",
65
- "@oh-my-pi/pi-natives": "16.4.1",
66
- "@oh-my-pi/pi-tui": "16.4.1",
67
- "@oh-my-pi/pi-utils": "16.4.1",
68
- "@oh-my-pi/pi-wire": "16.4.1",
69
- "@oh-my-pi/snapcompact": "16.4.1",
59
+ "@oh-my-pi/hashline": "16.4.2",
60
+ "@oh-my-pi/omp-stats": "16.4.2",
61
+ "@oh-my-pi/pi-agent-core": "16.4.2",
62
+ "@oh-my-pi/pi-ai": "16.4.2",
63
+ "@oh-my-pi/pi-catalog": "16.4.2",
64
+ "@oh-my-pi/pi-mnemopi": "16.4.2",
65
+ "@oh-my-pi/pi-natives": "16.4.2",
66
+ "@oh-my-pi/pi-tui": "16.4.2",
67
+ "@oh-my-pi/pi-utils": "16.4.2",
68
+ "@oh-my-pi/pi-wire": "16.4.2",
69
+ "@oh-my-pi/snapcompact": "16.4.2",
70
70
  "@opentelemetry/api": "^1.9.1",
71
71
  "@opentelemetry/context-async-hooks": "^2.7.1",
72
72
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -76,7 +76,7 @@
76
76
  "@puppeteer/browsers": "^3.0.4",
77
77
  "@types/turndown": "5.0.6",
78
78
  "@xterm/headless": "^6.0.0",
79
- "arktype": "^2.2.0",
79
+ "arktype": "2.2.2",
80
80
  "chalk": "^5.6.2",
81
81
  "diff": "^9.0.0",
82
82
  "fast-xml-parser": "^5.9.0",
@@ -11,7 +11,7 @@ import { logger } from "@oh-my-pi/pi-utils";
11
11
  import type { SourceMeta } from "../capability/types";
12
12
  import { resolveConfigValue } from "../config/resolve-config-value";
13
13
  import type { CustomTool } from "../extensibility/custom-tools/types";
14
- import type { AuthStorage } from "../session/auth-storage";
14
+ import { type AuthStorage, REMOTE_REFRESH_SENTINEL } from "../session/auth-storage";
15
15
  import {
16
16
  connectToServer,
17
17
  disconnectServer,
@@ -1226,76 +1226,79 @@ export class MCPManager {
1226
1226
  const { credentialId } = lookup;
1227
1227
  try {
1228
1228
  let credential: MCPStoredOAuthCredential | undefined = lookup.credential;
1229
- // Refresh material comes from ONE source: the credential's embedded
1230
- // fields (written atomically with the tokens they minted — tokenUrl
1231
- // always present) or, for legacy rows that predate embedding, the
1232
- // config auth block. Never mix the two: a shared file's auth block
1233
- // can belong to another profile, whose client the grant is NOT
1234
- // bound to.
1235
- const material = selectMcpOAuthRefreshMaterial(credential, auth);
1236
- const tokenUrl = material?.tokenUrl;
1237
- const clientId = material?.clientId;
1238
- const clientSecret = material?.clientSecret;
1239
- // `authorizationUrl` only lives on the embedded credential form;
1240
- // legacy `MCPAuthConfig` rows never carried it. Required to filter
1241
- // same-origin resource indicators on refresh when the authorize and
1242
- // token endpoints sit on different origins (issue #3502 review
1243
- // follow-up).
1244
- const authorizationUrl = material && "authorizationUrl" in material ? material.authorizationUrl : undefined;
1245
- const resourceIsFallback =
1246
- !material?.resource && (config.type === "http" || config.type === "sse") && Boolean(config.url);
1247
- const resource = material?.resource ?? (resourceIsFallback ? config.url : undefined);
1248
- // Proactive refresh: 5-minute buffer before expiry
1249
- // Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
1250
1229
  const REFRESH_BUFFER_MS = 5 * 60_000;
1251
- const shouldRefresh =
1252
- opts?.forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
1253
- if (shouldRefresh && credential.refresh && tokenUrl) {
1254
- try {
1255
- const refreshed = await refreshMCPOAuthToken(
1256
- tokenUrl,
1257
- credential.refresh,
1258
- clientId,
1259
- clientSecret,
1260
- resource,
1261
- { authorizationUrl, stripSameOriginResource: resourceIsFallback },
1262
- );
1263
- // Spread the old credential first so embedded refresh material survives rotation.
1264
- const refreshedCredential: MCPStoredOAuthCredential = {
1265
- ...credential,
1266
- ...refreshed,
1267
- tokenUrl,
1268
- clientId,
1269
- clientSecret,
1270
- resource: resourceIsFallback ? undefined : resource,
1271
- authorizationUrl,
1272
- };
1273
- await this.#authStorage.set(credentialId, refreshedCredential);
1274
- credential = refreshedCredential;
1275
- } catch (refreshError) {
1276
- const errorMsg = refreshError instanceof Error ? refreshError.message : String(refreshError);
1277
- if (isDefinitiveOAuthFailure(errorMsg)) {
1278
- // `invalid_grant` / `invalid_token` / 401 from the token endpoint means
1279
- // the server has retired this credential — keeping the stale access
1280
- // token would just re-fail with 401 on every MCP request and leave a
1281
- // poisoned row in agent.db that survives restarts. Drop it now so the
1282
- // next connect attempt surfaces a clean "needs reauth" failure and
1283
- // the user can recover with `/mcp reauth <server>` (or `/mcp unauth`
1284
- // to forget the server entirely).
1285
- logger.warn("MCP OAuth refresh failed definitively; cleared credential", {
1286
- credentialId,
1287
- error: errorMsg,
1230
+ const refreshResult = await this.#authStorage.refreshStoredOAuthCredential<MCPStoredOAuthCredential>(
1231
+ credentialId,
1232
+ {
1233
+ observedCredential: credential,
1234
+ credentialFromRow: row => row,
1235
+ forceRefresh: opts?.forceRefresh,
1236
+ refreshSkewMs: REFRESH_BUFFER_MS,
1237
+ canRefresh: current => {
1238
+ const material = selectMcpOAuthRefreshMaterial(current, auth);
1239
+ return Boolean(current.refresh && material?.tokenUrl);
1240
+ },
1241
+ refresh: (current, signal) => {
1242
+ if (current.refresh === REMOTE_REFRESH_SENTINEL) {
1243
+ throw new Error("MCP OAuth refresh token is broker-redacted; local refresh is unavailable");
1244
+ }
1245
+ const material = selectMcpOAuthRefreshMaterial(current, auth);
1246
+ const tokenUrl = material?.tokenUrl;
1247
+ if (!current.refresh || !tokenUrl) {
1248
+ throw new Error("MCP OAuth credential is missing refresh material");
1249
+ }
1250
+ const clientId = material?.clientId;
1251
+ const clientSecret = material?.clientSecret;
1252
+ const authorizationUrl =
1253
+ material && "authorizationUrl" in material ? material.authorizationUrl : undefined;
1254
+ const resourceIsFallback =
1255
+ !material?.resource && (config.type === "http" || config.type === "sse") && Boolean(config.url);
1256
+ const resource = material?.resource ?? (resourceIsFallback ? config.url : undefined);
1257
+ return refreshMCPOAuthToken(tokenUrl, current.refresh, clientId, clientSecret, resource, {
1258
+ authorizationUrl,
1259
+ stripSameOriginResource: resourceIsFallback,
1260
+ signal,
1288
1261
  });
1289
- await this.#authStorage.remove(credentialId);
1290
- credential = undefined;
1291
- } else {
1262
+ },
1263
+ mergeRefreshedCredential: (current, refreshed) => {
1264
+ const material = selectMcpOAuthRefreshMaterial(current, auth);
1265
+ const tokenUrl = material?.tokenUrl;
1266
+ const clientId = material?.clientId;
1267
+ const clientSecret = material?.clientSecret;
1268
+ const authorizationUrl =
1269
+ material && "authorizationUrl" in material ? material.authorizationUrl : undefined;
1270
+ const resourceIsFallback =
1271
+ !material?.resource && (config.type === "http" || config.type === "sse") && Boolean(config.url);
1272
+ const resource = material?.resource ?? (resourceIsFallback ? config.url : undefined);
1273
+ return {
1274
+ ...current,
1275
+ ...refreshed,
1276
+ tokenUrl,
1277
+ clientId,
1278
+ clientSecret,
1279
+ resource: resourceIsFallback ? undefined : resource,
1280
+ authorizationUrl,
1281
+ };
1282
+ },
1283
+ isDefinitiveFailure: error =>
1284
+ isDefinitiveOAuthFailure(error instanceof Error ? error.message : String(error)),
1285
+ disabledCause: error =>
1286
+ `oauth refresh failed: ${error instanceof Error ? error.message : String(error)}`,
1287
+ keepCredentialOnRefreshFailure: error =>
1288
+ !(error instanceof Error && error.message.includes("broker-redacted")),
1289
+ onRefreshFailure: refreshError => {
1290
+ if (refreshError instanceof Error && refreshError.message.includes("broker-redacted")) return;
1292
1291
  logger.warn("MCP OAuth refresh failed, using existing token", {
1293
1292
  credentialId,
1294
1293
  error: refreshError,
1295
1294
  });
1296
- }
1297
- }
1295
+ },
1296
+ },
1297
+ );
1298
+ if (refreshResult.removed) {
1299
+ logger.warn("MCP OAuth refresh failed definitively; cleared credential", { credentialId });
1298
1300
  }
1301
+ credential = refreshResult.credential;
1299
1302
 
1300
1303
  if (credential) {
1301
1304
  if (resolved.type === "http" || resolved.type === "sse") {
@@ -724,6 +724,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
724
724
  */
725
725
  export interface RefreshMCPOAuthTokenOptions {
726
726
  fetch?: FetchImpl;
727
+ signal?: AbortSignal;
727
728
  /**
728
729
  * Authorization-server URL the original grant was minted against. Used to
729
730
  * filter same-origin resource indicators on refresh. Defaults to `tokenUrl`'s
@@ -775,6 +776,7 @@ export async function refreshMCPOAuthToken(
775
776
  method: "POST",
776
777
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
777
778
  body: params.toString(),
779
+ signal: optsFromTrailing?.signal,
778
780
  });
779
781
 
780
782
  if (!response.ok) {
@@ -125,7 +125,6 @@ You NEVER open a file hoping. Hope is not a strategy.
125
125
  {{#has tools "grep"}}- Use `{{toolRefs.grep}}` to locate targets.{{/has}}
126
126
  {{#has tools "glob"}}- Use `{{toolRefs.glob}}` to map structure.{{/has}}
127
127
  {{#has tools "read"}}- Use `{{toolRefs.read}}` with offset/limit instead of whole-file reads.{{/has}}
128
- {{#has tools "task"}}- Use `{{toolRefs.task}}` to map unknown code instead of reading file after file yourself.{{/has}}
129
128
 
130
129
  {{#has tools "lsp"}}
131
130
  # LSP
@@ -142,9 +141,16 @@ You SHOULD use syntax-aware tools before text hacks:
142
141
  - Use `grep` only for plain-text lookup when structure is irrelevant.
143
142
  {{/ifAny}}
144
143
 
144
+ {{#has tools "task"}}
145
145
  # Delegation
146
+ {{#if useCodexTaskPrompt}}
147
+ {{#if eagerTasks}}
148
+ Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.
149
+ {{else}}
150
+ Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.
151
+ {{/if}}
152
+ {{else}}
146
153
  {{#if eagerTasks}}
147
- {{#has tools "task"}}
148
154
  {{#if eagerTasksAlways}}
149
155
  Delegation is the default here, not the exception. Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents rather than doing it yourself. Work alone ONLY when one of these is unambiguously true:
150
156
  - A single-file edit under approximately 30 lines
@@ -153,8 +159,17 @@ Delegation is the default here, not the exception. Once the design is settled, y
153
159
 
154
160
  Everything else—multi-file changes, refactors, new features, tests, investigations—MUST be decomposed and delegated.{{#if taskBatch}} Batch independent slices into one parallel `{{toolRefs.task}}` call; never serialize what can run concurrently.{{/if}}{{else}}Delegation is preferred here. Once the design is settled, you SHOULD fan substantial work out to `{{toolRefs.task}}` subagents instead of doing everything yourself. Multi-file changes, refactors, new features, tests, and investigations are strong candidates. Use your judgment for small, single-file, or interactive work.{{#if taskBatch}} When you delegate independent slices, batch them into one parallel `{{toolRefs.task}}` call rather than serializing them.{{/if}}
155
161
  {{/if}}
156
- {{/has}}
157
162
  {{/if}}
163
+ - Use `{{toolRefs.task}}` to map unknown code instead of reading file after file yourself.
164
+ - NEVER abandon phases under scope pressure—delegate, don't shrink.
165
+ - Default to parallel for complex changes. Delegate via `{{toolRefs.task}}` for non-importing file edits, multi-subsystem investigation, and decomposable work.
166
+ - **Maximize parallelism:** Break work into the widest possible {{#if taskBatch}}array of `tasks[]`{{else}}set of parallel `task` calls{{/if}}. NEVER serialize work that can run concurrently. Tasks touching different files or independent refactors should run in parallel; agents resolve their own file collisions live.
167
+ {{#when MAX_CONCURRENCY ">" 0}}
168
+ - **Concurrency cap:** At most {{pluralize MAX_CONCURRENCY "subagent" "subagents"}} run at once in this session — anything beyond that just queues, so a {{#if taskBatch}}`tasks[]` batch{{else}}set of parallel `task` calls{{/if}} larger than {{MAX_CONCURRENCY}} only delays results. Keep the fan-out at or under the cap.
169
+ {{/when}}
170
+ - **Sequence only when necessary:** The only reason to run A before B is if B strictly requires A's output to function (e.g., a core API contract or schema migration). {{#if taskIrcEnabled}}If the missing piece is small, run them in parallel and have B ask A via `irc`!{{/if}}
171
+ {{/if}}
172
+ {{/has}}
158
173
 
159
174
  EXECUTION WORKFLOW
160
175
  ==============
@@ -170,8 +185,6 @@ EXECUTION WORKFLOW
170
185
 
171
186
  # 3. Decompose
172
187
  - Update todos as you go; skip them for trivial requests. Marking a todo done is a transition: start the next in the same turn.
173
- - NEVER abandon phases under scope pressure—delegate, don't shrink.
174
- {{#has tools "task"}}- Default to parallel for complex changes. Delegate via `{{toolRefs.task}}` for non-importing file edits, multi-subsystem investigation, and decomposable work.{{/has}}
175
188
  - Plan only what makes the request work. Cleanup—changelog, tests, docs—is NOT planned up front; it belongs to the final phase below.
176
189
 
177
190
  # 4. Implement
@@ -2,13 +2,7 @@
2
2
  Execution does not block your turn: you receive agent and job IDs immediately, and the final results deliver themselves when the subagents finish.{{else}}{{#if batchEnabled}}Run subagents synchronously by passing items in a `tasks[]` batch.{{else}}Run ONE subagent synchronously per call.{{/if}}
3
3
  Execution blocks your turn: the call only returns once the work is completely finished.{{/if}}
4
4
 
5
- # Delegation Strategy
6
- - **Maximize parallelism:** Break work into the widest possible {{#if batchEnabled}}array of `tasks[]`{{else}}set of parallel `task` calls{{/if}}. NEVER serialize work that can run concurrently. Tasks touching different files or independent refactors should run in parallel; agents resolve their own file collisions live.
7
- {{#when MAX_CONCURRENCY ">" 0}}
8
- - **Concurrency cap:** At most {{pluralize MAX_CONCURRENCY "subagent" "subagents"}} run at once in this session — anything beyond that just queues, so a {{#if batchEnabled}}`tasks[]` batch{{else}}set of parallel `task` calls{{/if}} larger than {{MAX_CONCURRENCY}} only delays results. Keep the fan-out at or under the cap.
9
- {{/when}}
10
- - **Sequence only when necessary:** The only reason to run A before B is if B strictly requires A's output to function (e.g., a core API contract or schema migration). {{#if ircEnabled}}If the missing piece is small, run them in parallel and have B ask A via `irc`!{{/if}}
11
- {{#if ircEnabled}}- **Steering delivery:** Parent-to-subagent IRC is delivered immediately as steering; subagents blocked in `job poll` / `irc wait` do not need to poll separately for it.{{/if}}
5
+ # Assignment Design
12
6
  - **Role matching:** Assign each subagent a specific `role` (e.g. "Security Reviewer", "DB Migrator"). Do not spawn generic workers.
13
7
  - **No overhead:** Each assignment MUST instruct its agent to skip formatters, linters, and project-wide test suites. You will run those once at the end.
14
8
  - **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `scout`) when the affected files are genuinely unknown.
@@ -37,6 +31,7 @@ Execution blocks your turn: the call only returns once the work is completely fi
37
31
 
38
32
  # Context and Communication
39
33
  Subagents start blank. They have no access to your conversation history.
34
+ {{#if ircEnabled}}- **Steering delivery:** Parent-to-subagent IRC is delivered immediately as steering; subagents blocked in `job poll` / `irc wait` do not need to poll separately for it.{{/if}}
40
35
  {{#if batchEnabled}}
41
36
  - Pass large payloads using `local://<path>` URIs, never inline text.
42
37
  {{else}}
package/src/sdk.ts CHANGED
@@ -191,6 +191,7 @@ import {
191
191
  import { normalizeToolName, normalizeToolNames } from "./tools/builtin-names";
192
192
  import { ToolContextStore } from "./tools/context";
193
193
  import { getImageGenTools } from "./tools/image-gen";
194
+ import { isIrcEnabled } from "./tools/irc";
194
195
  import { wrapToolWithMetaNotice } from "./tools/output-meta";
195
196
  import { queueResolveHandler } from "./tools/resolve";
196
197
  import { ttsTool } from "./tools/tts";
@@ -2444,11 +2445,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2444
2445
  eagerTasks,
2445
2446
  eagerTasksAlways,
2446
2447
  taskBatch: settings.get("task.batch"),
2448
+ taskMaxConcurrency: settings.get("task.maxConcurrency"),
2449
+ taskIrcEnabled: isIrcEnabled(settings, options.taskDepth ?? 0),
2447
2450
  secretsEnabled,
2448
2451
  workspaceTree: workspaceTreePromise,
2449
2452
  includeWorkspaceTree,
2450
2453
  memoryRootEnabled: memoryBackend.id === "local",
2451
- model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined,
2454
+ model: getActiveModelString(),
2455
+ includeModelInPrompt: settings.get("includeModelInPrompt"),
2452
2456
  personality: agentKind === "sub" ? "none" : settings.get("personality"),
2453
2457
  renderMermaid: settings.get("tui.renderMermaid"),
2454
2458
  activeRepoContext,
@@ -286,6 +286,7 @@ import {
286
286
  type SecretObfuscator,
287
287
  } from "../secrets/obfuscator";
288
288
  import { invalidateHostMetadata } from "../ssh/connection-manager";
289
+ import { usesCodexTaskPrompt } from "../task/prompt-policy";
289
290
  import {
290
291
  AUTO_THINKING,
291
292
  type ConfiguredThinkingLevel,
@@ -6123,19 +6124,17 @@ export class AgentSession {
6123
6124
  return resolveEditMode(this.#getEditModeSession());
6124
6125
  }
6125
6126
 
6126
- /**
6127
- * Model key (`provider/id`) currently surfaced in the system prompt, or
6128
- * undefined when the model is unset or `includeModelInPrompt` is disabled.
6129
- */
6127
+ /** Cache key for model-dependent prompt content: displayed id or hidden-policy cohort. */
6130
6128
  #currentPromptModelKey(): string | undefined {
6131
- if (!this.settings.get("includeModelInPrompt")) return undefined;
6132
- return this.model ? formatModelString(this.model) : undefined;
6129
+ const model = this.model ? formatModelString(this.model) : undefined;
6130
+ if (!model || this.settings.get("includeModelInPrompt")) return model;
6131
+ return usesCodexTaskPrompt(model) ? "task-policy:gpt-5.6" : "task-policy:default";
6133
6132
  }
6134
6133
 
6135
6134
  async #syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
6136
6135
  const currentEditMode = this.#resolveActiveEditMode();
6137
6136
  const editModeChanged = previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit");
6138
- // The system prompt may surface the active model; a switch makes the cached prompt stale.
6137
+ // The system prompt selects model-specific policy even when it does not display the model id.
6139
6138
  const modelChanged = this.#currentPromptModelKey() !== this.#promptModelKey;
6140
6139
  if (editModeChanged || modelChanged) {
6141
6140
  await this.refreshBaseSystemPrompt();
@@ -15,6 +15,7 @@ import {
15
15
  getSessionsDir,
16
16
  isEnoent,
17
17
  logger,
18
+ stringifyJson,
18
19
  toError,
19
20
  } from "@oh-my-pi/pi-utils";
20
21
  import { ArtifactManager } from "./artifacts";
@@ -539,7 +540,7 @@ export class SessionManager {
539
540
  }
540
541
 
541
542
  #lineFor(entry: FileEntry): string {
542
- return `${JSON.stringify(prepareEntryForPersistence(entry, this.#blobs))}\n`;
543
+ return `${stringifyJson(prepareEntryForPersistence(entry, this.#blobs)) ?? "null"}\n`;
543
544
  }
544
545
 
545
546
  #titleSlotLine(): string {
@@ -22,6 +22,8 @@ import friendlyPersonality from "./prompts/system/personalities/friendly.md" wit
22
22
  import pragmaticPersonality from "./prompts/system/personalities/pragmatic.md" with { type: "text" };
23
23
  import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
24
24
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
25
+ import { normalizeConcurrencyLimit } from "./task/parallel";
26
+ import { usesCodexTaskPrompt } from "./task/prompt-policy";
25
27
  import { shortenPath } from "./tools/render-utils";
26
28
  import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
27
29
  import { formatLocalCalendarDate } from "./utils/local-date";
@@ -481,8 +483,12 @@ export interface BuildSystemPromptOptions {
481
483
  eagerTasks?: boolean;
482
484
  /** When true, the Eager Tasks section uses the hard MUST/ONLY wording (`task.eager: always`) rather than the softer `preferred` nudge. */
483
485
  eagerTasksAlways?: boolean;
484
- /** Whether `task.batch` is enabled; gates batch-call guidance in the Eager Tasks section. */
486
+ /** Whether `task.batch` is enabled; selects the centralized delegation guidance's call shape. */
485
487
  taskBatch?: boolean;
488
+ /** Effective task concurrency limit displayed in centralized delegation guidance. Zero means unlimited. */
489
+ taskMaxConcurrency?: number;
490
+ /** Whether IRC-backed parallel coordination can be included in delegation policy. */
491
+ taskIrcEnabled?: boolean;
486
492
  /** Rules with alwaysApply=true — their full content is injected into the prompt. */
487
493
  alwaysApplyRules?: AlwaysApplyRule[];
488
494
  /** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
@@ -491,8 +497,10 @@ export interface BuildSystemPromptOptions {
491
497
  workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
492
498
  /** Whether the local memory://root summary is active. */
493
499
  memoryRootEnabled?: boolean;
494
- /** Active model identifier (e.g. "anthropic/claude-opus-4") surfaced to the agent. */
500
+ /** Active model identifier (e.g. "anthropic/claude-opus-4") used by prompt policy and optionally surfaced. */
495
501
  model?: string;
502
+ /** Whether to surface `model` in the workstation block. Model-specific prompt policy still uses it. Default: true. */
503
+ includeModelInPrompt?: boolean;
496
504
  /** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */
497
505
  personality?: Personality;
498
506
  /** Whether to include the workspace directory tree in the system prompt. Default: false */
@@ -536,10 +544,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
536
544
  eagerTasks = false,
537
545
  eagerTasksAlways = false,
538
546
  taskBatch = true,
547
+ taskMaxConcurrency = 0,
548
+ taskIrcEnabled = false,
539
549
  secretsEnabled = false,
540
550
  workspaceTree: providedWorkspaceTree,
541
551
  memoryRootEnabled = false,
542
552
  model,
553
+ includeModelInPrompt = true,
543
554
  personality = "default",
544
555
  includeWorkspaceTree = false,
545
556
  renderMermaid = true,
@@ -770,7 +781,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
770
781
  date,
771
782
  dateTime,
772
783
  cwd: promptCwd,
773
- model: model ?? "",
784
+ model: includeModelInPrompt ? (model ?? "") : "",
785
+ useCodexTaskPrompt: usesCodexTaskPrompt(model),
774
786
  personality: personality === "none" ? "" : PERSONALITY_SPECS[personality].trim(),
775
787
  intentTracing: !!intentField,
776
788
  intentField: intentField ?? "",
@@ -780,6 +792,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
780
792
  eagerTasks,
781
793
  eagerTasksAlways,
782
794
  taskBatch,
795
+ MAX_CONCURRENCY: normalizeConcurrencyLimit(taskMaxConcurrency),
796
+ taskIrcEnabled,
783
797
  secretsEnabled,
784
798
  hasMemoryRoot: memoryRootEnabled,
785
799
  hasObsidian: hasObsidian(),
package/src/task/index.ts CHANGED
@@ -60,7 +60,7 @@ import {
60
60
  } from "./isolation-runner";
61
61
  import { generateTaskName } from "./name-generator";
62
62
  import { AgentOutputManager } from "./output-manager";
63
- import { mapWithConcurrencyLimit, normalizeConcurrencyLimit, Semaphore } from "./parallel";
63
+ import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
64
64
  import { renderResult, renderCall as renderTaskCall } from "./render";
65
65
  import { repairTaskParams } from "./repair-args";
66
66
  import { parseIsolationMode } from "./worktree";
@@ -180,7 +180,6 @@ export function formatResultOutputFallback(result: Pick<SingleResult, "output" |
180
180
  */
181
181
  function renderDescription(
182
182
  agents: AgentDefinition[],
183
- maxConcurrency: number,
184
183
  isolationEnabled: boolean,
185
184
  disabledAgents: string[],
186
185
  batchEnabled: boolean,
@@ -208,7 +207,6 @@ function renderDescription(
208
207
  defaultAgent: spawnPolicy.defaultAgent,
209
208
  defaultAgentIsGeneric: spawnPolicy.defaultAgent === DEFAULT_SPAWN_AGENT,
210
209
  allowedAgentsText: spawnPolicy.allowedPromptText,
211
- MAX_CONCURRENCY: normalizeConcurrencyLimit(maxConcurrency),
212
210
  isolationEnabled,
213
211
  batchEnabled,
214
212
  asyncEnabled,
@@ -521,11 +519,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
521
519
  /** Dynamic description that reflects current disabled-agent settings */
522
520
  get description(): string {
523
521
  const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
524
- const maxConcurrency = this.session.settings.get("task.maxConcurrency");
525
522
  const isolationMode = this.session.settings.get("task.isolation.mode");
526
523
  return renderDescription(
527
524
  this.#discoveredAgents,
528
- maxConcurrency,
529
525
  isolationMode !== "none",
530
526
  disabledAgents,
531
527
  this.#isBatchEnabled(),
@@ -0,0 +1,8 @@
1
+ import { bareModelId, parseOpenAIModel, semverEqual } from "@oh-my-pi/pi-catalog/identity";
2
+
3
+ /** Whether task guidance should follow Codex's GPT-5.6-specific delegation policy. */
4
+ export function usesCodexTaskPrompt(modelId: string | undefined): boolean {
5
+ if (!modelId) return false;
6
+ const parsed = parseOpenAIModel(bareModelId(modelId));
7
+ return parsed !== null && semverEqual(parsed.version, "5.6");
8
+ }