@gajae-code/coding-agent 0.12.8 → 0.12.10

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/cli/notify-cli.d.ts +1 -0
  3. package/dist/types/commands/sdk.d.ts +23 -0
  4. package/dist/types/discovery/agents-md.d.ts +11 -1
  5. package/dist/types/modes/types.d.ts +6 -0
  6. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  7. package/dist/types/sdk/bus/notification-service.d.ts +23 -0
  8. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  9. package/dist/types/session/internal/managed-session-storage.d.ts +8 -0
  10. package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
  11. package/dist/types/system-prompt.d.ts +12 -1
  12. package/dist/types/tools/composer-bash-policy.d.ts +9 -1
  13. package/package.json +8 -8
  14. package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
  15. package/src/cli/notify-cli.ts +12 -6
  16. package/src/commands/sdk.ts +54 -0
  17. package/src/config/model-profiles.ts +8 -0
  18. package/src/config/model-registry.ts +4 -1
  19. package/src/discovery/agents-md.ts +76 -19
  20. package/src/gjc-runtime/psmux-detect.ts +12 -16
  21. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  22. package/src/internal-urls/docs-index.generated.ts +4 -3
  23. package/src/modes/components/extensions/inspector-panel.ts +9 -5
  24. package/src/modes/controllers/selector-controller.ts +61 -43
  25. package/src/modes/print-mode.ts +3 -0
  26. package/src/modes/types.ts +6 -0
  27. package/src/prompts/system/system-prompt.md +1 -0
  28. package/src/sdk/bus/daemon-paths.ts +2 -0
  29. package/src/sdk/bus/notification-service.ts +140 -7
  30. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  31. package/src/sdk/session.ts +24 -7
  32. package/src/session/agent-session.ts +65 -13
  33. package/src/session/internal/managed-session-storage.ts +36 -2
  34. package/src/session/internal/native-publish-outcome.ts +12 -2
  35. package/src/slash-commands/builtin-registry.ts +35 -11
  36. package/src/system-prompt.ts +28 -15
  37. package/src/tools/bash.ts +3 -1
  38. package/src/tools/composer-bash-policy.ts +21 -5
  39. package/src/utils/command-args.ts +19 -30
  40. package/src/utils/lang-from-path.ts +2 -4
@@ -9,6 +9,10 @@ import { theme } from "../../../modes/theme/theme";
9
9
  import { shortenPath } from "../../../tools/render-utils";
10
10
  import type { Extension, ExtensionState } from "./types";
11
11
 
12
+ function renderSeparator(width: number): string {
13
+ return theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.max(0, Math.min(width - 2, 40))));
14
+ }
15
+
12
16
  export class InspectorPanel implements Component {
13
17
  #extension: Extension | null = null;
14
18
 
@@ -106,7 +110,7 @@ export class InspectorPanel implements Component {
106
110
  #renderFilePreview(raw: unknown, width: number): string[] {
107
111
  const lines: string[] = [];
108
112
  lines.push(theme.fg("muted", "Preview:"));
109
- lines.push(theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.min(width - 2, 40))));
113
+ lines.push(renderSeparator(width));
110
114
 
111
115
  const content = this.#getContextFileContent(raw);
112
116
  if (!content) {
@@ -164,7 +168,7 @@ export class InspectorPanel implements Component {
164
168
  #renderToolArgs(raw: unknown, width: number): string[] {
165
169
  const lines: string[] = [];
166
170
  lines.push(theme.fg("muted", "Arguments:"));
167
- lines.push(theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.min(width - 2, 40))));
171
+ lines.push(renderSeparator(width));
168
172
 
169
173
  try {
170
174
  const tool = raw as any;
@@ -203,7 +207,7 @@ export class InspectorPanel implements Component {
203
207
  #renderSkillContent(raw: unknown, width: number): string[] {
204
208
  const lines: string[] = [];
205
209
  lines.push(theme.fg("muted", "Instruction:"));
206
- lines.push(theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.min(width - 2, 40))));
210
+ lines.push(renderSeparator(width));
207
211
 
208
212
  try {
209
213
  const skill = raw as any;
@@ -232,7 +236,7 @@ export class InspectorPanel implements Component {
232
236
  #renderMcpDetails(raw: unknown, width: number): string[] {
233
237
  const lines: string[] = [];
234
238
  lines.push(theme.fg("muted", "Connection:"));
235
- lines.push(theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.min(width - 2, 40))));
239
+ lines.push(renderSeparator(width));
236
240
 
237
241
  try {
238
242
  const mcp = raw as any;
@@ -271,7 +275,7 @@ export class InspectorPanel implements Component {
271
275
  // Show trigger pattern if present
272
276
  if (ext.trigger) {
273
277
  lines.push(theme.fg("muted", "Trigger:"));
274
- lines.push(theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.min(width - 2, 40))));
278
+ lines.push(renderSeparator(width));
275
279
  lines.push(` ${theme.fg("accent", ext.trigger)}`);
276
280
  lines.push("");
277
281
  }
@@ -201,6 +201,11 @@ const CALLBACK_SERVER_PROVIDERS = new Set<string>([
201
201
 
202
202
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
203
203
 
204
+ /** Providers whose authorization server can display a code for `/login <provider> --manual`. */
205
+ const MANUAL_CODE_PROVIDERS = new Set<string>(["anthropic"]);
206
+
207
+ const MANUAL_CODE_LOGIN_TIP = "Tip: You can complete pairing with /login <code>.";
208
+
204
209
  function isThemePreviewSuperseded(result: { success: boolean; error?: string }): boolean {
205
210
  return !result.success && result.error?.includes("superseded by a newer request") === true;
206
211
  }
@@ -2945,7 +2950,14 @@ export class SelectorController {
2945
2950
  });
2946
2951
  }
2947
2952
 
2948
- async #handleOAuthLogin(providerId: string): Promise<void> {
2953
+ async #handleOAuthLogin(providerId: string, options?: OAuthSelectorOptions): Promise<void> {
2954
+ const manualCode = options?.manualCode === true;
2955
+ if (manualCode && !MANUAL_CODE_PROVIDERS.has(providerId)) {
2956
+ this.ctx.showError(
2957
+ `${providerId} has no paste-a-code login. Supported: ${[...MANUAL_CODE_PROVIDERS].join(", ")}.`,
2958
+ );
2959
+ return;
2960
+ }
2949
2961
  this.ctx.showStatus(`Logging in to ${providerId}…`);
2950
2962
  const manualInput = this.ctx.oauthManualInput;
2951
2963
  const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
@@ -2953,51 +2965,57 @@ export class SelectorController {
2953
2965
  this.ctx.showStatus("Checking the local OpenCodex proxy…");
2954
2966
  }
2955
2967
  try {
2956
- await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
2957
- onAuth: (info: { url: string; instructions?: string }) => {
2958
- this.ctx.chatContainer.addChild(new Spacer(1));
2959
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
2960
- const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
2961
- this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
2962
- if (info.instructions) {
2968
+ await this.ctx.session.modelRegistry.authStorage.login(
2969
+ providerId as OAuthProvider,
2970
+ {
2971
+ onAuth: (info: { url: string; instructions?: string }) => {
2963
2972
  this.ctx.chatContainer.addChild(new Spacer(1));
2964
- this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
2965
- }
2966
- if (useManualInput) {
2973
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
2974
+ const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
2975
+ this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
2976
+ if (info.instructions) {
2977
+ this.ctx.chatContainer.addChild(new Spacer(1));
2978
+ this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
2979
+ }
2980
+ if (useManualInput) {
2981
+ this.ctx.chatContainer.addChild(new Spacer(1));
2982
+ this.ctx.chatContainer.addChild(
2983
+ new Text(theme.fg("dim", manualCode ? MANUAL_CODE_LOGIN_TIP : MANUAL_LOGIN_TIP), 1, 0),
2984
+ );
2985
+ }
2986
+ this.ctx.ui.requestRender();
2987
+ this.ctx.openInBrowser(info.url);
2988
+ },
2989
+ onPrompt: async (prompt: { message: string; placeholder?: string }) => {
2967
2990
  this.ctx.chatContainer.addChild(new Spacer(1));
2968
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", MANUAL_LOGIN_TIP), 1, 0));
2969
- }
2970
- this.ctx.ui.requestRender();
2971
- this.ctx.openInBrowser(info.url);
2972
- },
2973
- onPrompt: async (prompt: { message: string; placeholder?: string }) => {
2974
- this.ctx.chatContainer.addChild(new Spacer(1));
2975
- this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
2976
- if (prompt.placeholder) {
2977
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
2978
- }
2979
- this.ctx.ui.requestRender();
2980
- const { promise, resolve } = Promise.withResolvers<string>();
2981
- const codeInput = new Input();
2982
- codeInput.onSubmit = () => {
2983
- const code = codeInput.getValue();
2991
+ this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
2992
+ if (prompt.placeholder) {
2993
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
2994
+ }
2995
+ this.ctx.ui.requestRender();
2996
+ const { promise, resolve } = Promise.withResolvers<string>();
2997
+ const codeInput = new Input();
2998
+ codeInput.onSubmit = () => {
2999
+ const code = codeInput.getValue();
3000
+ this.ctx.editorContainer.clear();
3001
+ this.ctx.editorContainer.addChild(this.ctx.editor);
3002
+ this.ctx.ui.setFocus(this.ctx.editor);
3003
+ resolve(code);
3004
+ };
2984
3005
  this.ctx.editorContainer.clear();
2985
- this.ctx.editorContainer.addChild(this.ctx.editor);
2986
- this.ctx.ui.setFocus(this.ctx.editor);
2987
- resolve(code);
2988
- };
2989
- this.ctx.editorContainer.clear();
2990
- this.ctx.editorContainer.addChild(codeInput);
2991
- this.ctx.ui.setFocus(codeInput);
2992
- this.ctx.ui.requestRender();
2993
- return promise;
2994
- },
2995
- onProgress: (message: string) => {
2996
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
2997
- this.ctx.ui.requestRender();
3006
+ this.ctx.editorContainer.addChild(codeInput);
3007
+ this.ctx.ui.setFocus(codeInput);
3008
+ this.ctx.ui.requestRender();
3009
+ return promise;
3010
+ },
3011
+ onProgress: (message: string) => {
3012
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
3013
+ this.ctx.ui.requestRender();
3014
+ },
3015
+ onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
2998
3016
  },
2999
- onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
3000
- });
3017
+ { manualCode },
3018
+ );
3001
3019
  await this.ctx.session.modelRegistry.refresh();
3002
3020
  this.ctx.chatContainer.addChild(new Spacer(1));
3003
3021
  const successMessage =
@@ -3050,7 +3068,7 @@ export class SelectorController {
3050
3068
  return;
3051
3069
  }
3052
3070
  if (mode === "login") {
3053
- await this.#handleOAuthLogin(providerId);
3071
+ await this.#handleOAuthLogin(providerId, options);
3054
3072
  } else {
3055
3073
  await this.#handleOAuthLogout(providerId);
3056
3074
  }
@@ -198,6 +198,9 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
198
198
  let unsubscribe: (() => void) | undefined;
199
199
 
200
200
  try {
201
+ for (const warning of new Set(session.configWarnings)) {
202
+ await writeStderrAndQuiesce(`Warning: ${warning}\n`);
203
+ }
201
204
  // Emit session header for JSON mode.
202
205
  if (mode === "json") {
203
206
  const header = session.sessionManager.getHeader();
@@ -495,4 +495,10 @@ export interface OAuthSelectorOptions {
495
495
  allowExternalCredentialDiscovery?: boolean;
496
496
  trigger?: "bare-login";
497
497
  externalCredentialDiscover?: CredentialAutoImportOptions["discover"];
498
+ /**
499
+ * Pair by pasting the code the provider displays instead of waiting on the
500
+ * loopback callback. Set by `/login <provider> --manual` for browsers that
501
+ * cannot reach this machine.
502
+ */
503
+ manualCode?: boolean;
498
504
  }
@@ -97,6 +97,7 @@ Do not claim a web search, browser action, integration, or subagent ran without
97
97
  <inputs>
98
98
  - Keep tool inputs concise where possible.
99
99
  - For `path` or path-like fields, prefer relative paths.
100
+ - Write non-ASCII text in tool inputs as literal UTF-8, NEVER as hand-spelled `\uXXXX` escapes, including JSON serialized into a string field (no `ensure_ascii`-style output there). Escapes that are the intended source syntax of the file you are writing — character-class ranges, codepoint bounds — are unaffected.
100
101
  {{#if intentTracing}}
101
102
  - Most tools have a `{{intentField}}` parameter. Fill it with a concise intent in present participle form, 2-6 words, no period, capitalized.
102
103
  {{/if}}
@@ -16,6 +16,7 @@ export interface DaemonPaths {
16
16
  ownership: string;
17
17
  roots: string;
18
18
  steal: string;
19
+ diagnostic: string;
19
20
  aliases: string;
20
21
  seenUpdates: string;
21
22
  }
@@ -31,6 +32,7 @@ export function daemonPaths(agentDir: string): DaemonPaths {
31
32
  roots: path.join(dir, "telegram-daemon.roots.json"),
32
33
  steal: path.join(dir, "telegram-daemon.steal"),
33
34
  aliases: path.join(dir, "telegram-callback-aliases.json"),
35
+ diagnostic: path.join(dir, "telegram-daemon.diagnostics.json"),
34
36
  seenUpdates: path.join(dir, "telegram-seen-updates.json"),
35
37
  };
36
38
  }
@@ -63,6 +63,49 @@ export function sanitizeDiagnostic(text: string, token?: string): string {
63
63
  return out.replace(TELEGRAM_TOKEN_PATTERN, "<redacted>");
64
64
  }
65
65
 
66
+ export interface NotificationDiagnosticEvent {
67
+ timestamp: string;
68
+ operation: string;
69
+ phase: string;
70
+ outcome: string;
71
+ reason: string;
72
+ pid?: number;
73
+ incarnation?: string;
74
+ ageMs?: number;
75
+ detail?: string;
76
+ }
77
+
78
+ /** Append a bounded, private, secret-safe daemon diagnostic event. */
79
+ export async function writeNotificationDiagnostic(
80
+ settings: Pick<Settings, "getAgentDir">,
81
+ event: Omit<NotificationDiagnosticEvent, "timestamp"> & { timestamp?: string },
82
+ ): Promise<void> {
83
+ const file = daemonPaths(settings.getAgentDir()).diagnostic;
84
+ const dir = path.dirname(file);
85
+ let events: NotificationDiagnosticEvent[] = [];
86
+ try {
87
+ const parsed = JSON.parse(await fsPromises.readFile(file, "utf8")) as { events?: unknown };
88
+ if (Array.isArray(parsed.events)) events = parsed.events as NotificationDiagnosticEvent[];
89
+ } catch {
90
+ // A missing or corrupt diagnostic history must not block notification recovery.
91
+ }
92
+ const safe: NotificationDiagnosticEvent = {
93
+ timestamp: event.timestamp ?? new Date().toISOString(),
94
+ operation: event.operation.slice(0, 80),
95
+ phase: event.phase.slice(0, 80),
96
+ outcome: event.outcome.slice(0, 80),
97
+ reason: event.reason.slice(0, 120),
98
+ ...(event.pid === undefined ? {} : { pid: event.pid }),
99
+ ...(event.incarnation === undefined ? {} : { incarnation: event.incarnation.slice(0, 120) }),
100
+ ...(event.ageMs === undefined ? {} : { ageMs: Math.max(0, Math.floor(event.ageMs)) }),
101
+ ...(event.detail === undefined ? {} : { detail: sanitizeDiagnostic(event.detail).slice(0, 512) }),
102
+ };
103
+ await fsPromises.mkdir(dir, { recursive: true, mode: 0o700 });
104
+ await fsPromises.writeFile(file, `${JSON.stringify({ version: 1, events: [...events, safe].slice(-64) })}\n`, {
105
+ mode: 0o600,
106
+ });
107
+ }
108
+
66
109
  function sanitizeProviderDiagnostic(text: string, cfg: NotificationConfig, provider: NotificationProvider): string {
67
110
  const secrets =
68
111
  provider === "telegram"
@@ -182,6 +225,7 @@ export interface NotificationServiceDeps {
182
225
  fs?: NotificationServiceFs;
183
226
  now?: () => number;
184
227
  pidAlive?: (pid: number) => boolean;
228
+ pidIncarnation?: (pid: number) => string | undefined;
185
229
  fetchImpl?: typeof fetch;
186
230
  apiBase?: string;
187
231
  createDiscordDiagnostic?: (config: { applicationId: string; botToken: string }) => DiscordDiagnosticProvider;
@@ -1126,6 +1170,9 @@ export interface NotificationRecoveryReport {
1126
1170
  action: DaemonRecoveryAction;
1127
1171
  detail: string;
1128
1172
  ownerId: string | undefined;
1173
+ blockingReason?: string;
1174
+ markerAgeMs?: number;
1175
+ forceCommand?: string;
1129
1176
  pid: number | undefined;
1130
1177
  };
1131
1178
  }
@@ -1134,6 +1181,7 @@ export interface RecoveryOptions {
1134
1181
  settings: Settings;
1135
1182
  stateRoot?: string;
1136
1183
  deps?: NotificationServiceDeps;
1184
+ forceDaemonLock?: boolean;
1137
1185
  }
1138
1186
 
1139
1187
  export interface DaemonTransitionLock {
@@ -1167,6 +1215,7 @@ type TransitionMarkerFs = {
1167
1215
  writeFile?(file: string, data: string, opts?: WriteFileOptions): Promise<void>;
1168
1216
  readEndpointFile?(file: string): Promise<NotificationEndpointFile>;
1169
1217
  exactUnlink?(file: string, identity: NotificationEndpointFileIdentity): Promise<NotificationExactUnlinkResult>;
1218
+ stat?(file: string): Promise<{ mtimeMs: number }>;
1170
1219
  };
1171
1220
 
1172
1221
  async function readTransitionMarker(
@@ -1318,6 +1367,36 @@ function defaultTransitionPidIncarnation(pid: number): string | undefined {
1318
1367
  return processIncarnation(pid);
1319
1368
  }
1320
1369
 
1370
+ const TRANSITION_MARKER_GRACE_MS = 3 * HEARTBEAT_TTL_MS;
1371
+
1372
+ async function detachStaleTransitionMarker(input: {
1373
+ fs: TransitionMarkerFs;
1374
+ path: string;
1375
+ now: number;
1376
+ pidAlive: (pid: number) => boolean;
1377
+ pidIncarnation: (pid: number) => string | undefined;
1378
+ }): Promise<boolean> {
1379
+ const snapshot = await readTransitionMarker(input.fs, input.path);
1380
+ if (!snapshot) return false;
1381
+ let marker: unknown;
1382
+ try {
1383
+ marker = JSON.parse(snapshot.raw);
1384
+ } catch {
1385
+ return false;
1386
+ }
1387
+ if (!isDaemonTransitionLock(marker)) return false;
1388
+ const currentIncarnation = input.pidIncarnation(marker.pid);
1389
+ if (
1390
+ input.pidAlive(marker.pid) ||
1391
+ !isProcessIncarnation(marker.incarnation) ||
1392
+ (isProcessIncarnation(currentIncarnation) && currentIncarnation === marker.incarnation)
1393
+ )
1394
+ return false;
1395
+ const ageMs = input.now - marker.createdAt;
1396
+ if (!Number.isFinite(ageMs) || ageMs <= TRANSITION_MARKER_GRACE_MS) return false;
1397
+ return await detachTransitionMarker(input.fs, input.path, snapshot);
1398
+ }
1399
+
1321
1400
  /**
1322
1401
  * Owner-bound removal of a dead daemon's lock. Closes the classic
1323
1402
  * check-then-unlink TOCTOU: a naive `unlink(lock)` after observing a dead owner
@@ -1332,20 +1411,36 @@ async function removeDeadOwnerLock(
1332
1411
  paths: DaemonPaths,
1333
1412
  pidAlive: (pid: number) => boolean,
1334
1413
  expected: NormalizedDaemonState,
1414
+ now: () => number,
1415
+ pidIncarnation: (pid: number) => string | undefined,
1335
1416
  ): Promise<"cleared" | "contended" | "superseded" | "now-alive" | "unlink-failed"> {
1336
- const transition = await acquireDaemonTransitionLock({
1417
+ let transition = await acquireDaemonTransitionLock({
1337
1418
  fs,
1338
1419
  path: paths.steal,
1339
1420
  pid: process.pid,
1340
1421
  pidAlive: defaultTransitionPidAlive,
1341
1422
  pidIncarnation: defaultTransitionPidIncarnation,
1342
1423
  });
1424
+ if (!transition) {
1425
+ await detachStaleTransitionMarker({
1426
+ fs,
1427
+ path: paths.steal,
1428
+ now: now(),
1429
+ pidAlive,
1430
+ pidIncarnation,
1431
+ });
1432
+ transition = await acquireDaemonTransitionLock({
1433
+ fs,
1434
+ path: paths.steal,
1435
+ pid: process.pid,
1436
+ pidAlive: defaultTransitionPidAlive,
1437
+ pidIncarnation: defaultTransitionPidIncarnation,
1438
+ });
1439
+ }
1343
1440
  if (!transition) return "contended";
1344
1441
  try {
1345
1442
  const current = await readDaemonStateFile(fs, paths.state);
1346
- if (!current || current.ownerId !== expected.ownerId || current.pid !== expected.pid) {
1347
- return "superseded";
1348
- }
1443
+ if (!current || current.ownerId !== expected.ownerId || current.pid !== expected.pid) return "superseded";
1349
1444
  if (pidAlive(current.pid)) return "now-alive";
1350
1445
  if (!(await daemonTransitionLockIsHeld({ fs, path: paths.steal, lock: transition }))) return "contended";
1351
1446
  try {
@@ -1373,6 +1468,7 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
1373
1468
  const deps = opts.deps ?? {};
1374
1469
  const fs = deps.fs ?? nodeServiceFs;
1375
1470
  const pidAlive = deps.pidAlive ?? defaultPidAlive;
1471
+ const now = deps.now ?? Date.now;
1376
1472
  const stateRoot = opts.stateRoot ?? defaultStateRoot();
1377
1473
 
1378
1474
  const dir = endpointDir(stateRoot);
@@ -1472,7 +1568,14 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
1472
1568
  pid: state.pid,
1473
1569
  };
1474
1570
  } else if (hasLock) {
1475
- const outcome = await removeDeadOwnerLock(fs, paths, pidAlive, state);
1571
+ const outcome = await removeDeadOwnerLock(
1572
+ fs,
1573
+ paths,
1574
+ pidAlive,
1575
+ state,
1576
+ now,
1577
+ deps.pidIncarnation ?? defaultTransitionPidIncarnation,
1578
+ );
1476
1579
  const action: DaemonRecoveryAction =
1477
1580
  outcome === "cleared"
1478
1581
  ? "cleared-dead-owner-lock"
@@ -1491,9 +1594,35 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
1491
1594
  : outcome === "superseded"
1492
1595
  ? "a new daemon owner took over during recovery; lock left untouched"
1493
1596
  : outcome === "contended"
1494
- ? "another daemon is starting or stealing the lock; lock left untouched"
1597
+ ? `recovery blocked by transition marker; lock left untouched${opts.forceDaemonLock ? " even after forced stale-marker retry" : ""}`
1495
1598
  : `could not remove lock of dead owner pid ${state.pid}`;
1496
- daemon = { action, detail, ownerId: state.ownerId, pid: state.pid };
1599
+ const blockingReason = outcome === "contended" ? "transition-marker-unavailable-or-contended" : undefined;
1600
+ const markerAgeMs =
1601
+ blockingReason && fs.stat
1602
+ ? (await fs.stat(paths.steal).catch(() => undefined))?.mtimeMs
1603
+ ? now() - (await fs.stat(paths.steal).catch(() => undefined))!.mtimeMs
1604
+ : undefined
1605
+ : undefined;
1606
+ if (blockingReason) {
1607
+ await writeNotificationDiagnostic(opts.settings, {
1608
+ operation: "notify.recovery",
1609
+ phase: "recovery",
1610
+ outcome: action,
1611
+ reason: blockingReason,
1612
+ pid: state.pid,
1613
+ ageMs: markerAgeMs,
1614
+ detail,
1615
+ });
1616
+ }
1617
+ daemon = {
1618
+ action,
1619
+ detail,
1620
+ ownerId: state.ownerId,
1621
+ pid: state.pid,
1622
+ ...(blockingReason
1623
+ ? { blockingReason, markerAgeMs, forceCommand: "gjc notify recovery --force-daemon-lock" }
1624
+ : {}),
1625
+ };
1497
1626
  } else {
1498
1627
  daemon = {
1499
1628
  action: "none",
@@ -1533,5 +1662,9 @@ export function formatNotificationRecoveryReport(report: NotificationRecoveryRep
1533
1662
  for (const unknown of report.endpointsRetainedUnknown ?? [])
1534
1663
  lines.push(` - retained unverified cleanup path ${unknown}`);
1535
1664
  lines.push(` daemon: ${report.daemon.action} — ${report.daemon.detail}`);
1665
+ if (report.daemon.blockingReason) lines.push(` - blocking reason: ${report.daemon.blockingReason}`);
1666
+ if (report.daemon.markerAgeMs !== undefined)
1667
+ lines.push(` - transition marker age: ${report.daemon.markerAgeMs}ms`);
1668
+ if (report.daemon.forceCommand) lines.push(` - safe escape: ${report.daemon.forceCommand}`);
1536
1669
  return lines.join("\n");
1537
1670
  }
@@ -55,10 +55,10 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
55
55
  * ownership, settlement, and descendant cleanup authority. Generation 38 also
56
56
  * adds durable provider-intent admission without changing owner, reclaim,
57
57
  * signal, or spawn authority.
58
- * Generation 41 applies first-class provider-settings admission to Telegram
58
+ * Generation 42 applies first-class provider-settings admission to Telegram
59
59
  * lifecycle controls.
60
60
  */
61
- export const DAEMON_GENERATION = 41;
61
+ export const DAEMON_GENERATION = 42;
62
62
 
63
63
  /**
64
64
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
@@ -136,6 +136,7 @@ import {
136
136
  buildSystemPrompt as buildSystemPromptInternal,
137
137
  buildSystemPromptToolMetadata,
138
138
  loadProjectContextFiles as loadContextFilesInternal,
139
+ loadProjectContextFilesResult as loadContextFilesResultInternal,
139
140
  } from "../system-prompt";
140
141
  import { AgentOutputManager } from "../task/output-manager";
141
142
  import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
@@ -1166,10 +1167,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1166
1167
  // Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
1167
1168
  // at their respective consumer sites. Their work can overlap with model resolution, secret loading,
1168
1169
  // session-context build, tool creation, MCP discovery, and extension discovery.
1169
- const contextFilesPromise = options.contextFiles
1170
- ? Promise.resolve(options.contextFiles)
1171
- : logger.time("discoverContextFiles", discoverContextFiles, cwd, agentDir);
1172
- contextFilesPromise.catch(() => {});
1170
+ const contextFilesResultPromise = options.contextFiles
1171
+ ? Promise.resolve({ contextFiles: options.contextFiles, warnings: [] })
1172
+ : logger.time("discoverContextFiles", loadContextFilesResultInternal, { cwd });
1173
+ contextFilesResultPromise.catch(() => {});
1173
1174
  const promptTemplatesPromise = options.promptTemplates
1174
1175
  ? Promise.resolve(options.promptTemplates)
1175
1176
  : logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir);
@@ -1465,10 +1466,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1465
1466
  }
1466
1467
  return result;
1467
1468
  };
1468
- const [contextFiles, resolvedWorkspaceTree] = await Promise.all([
1469
- contextFilesPromise,
1469
+ const [contextFilesResult, resolvedWorkspaceTree] = await Promise.all([
1470
+ contextFilesResultPromise,
1470
1471
  raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
1471
1472
  ]);
1473
+ const contextFiles = contextFilesResult.contextFiles;
1474
+ const discoveredContextFileWarnings = contextFilesResult.warnings;
1472
1475
 
1473
1476
  const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
1474
1477
  const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
@@ -2233,6 +2236,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2233
2236
  settings.get("tools.intentTracing"),
2234
2237
  options.hasUI ?? false,
2235
2238
  );
2239
+ const emittedContextFileWarnings = new Set(discoveredContextFileWarnings);
2240
+ const contextFileWarnings = [...discoveredContextFileWarnings];
2241
+ for (const warning of discoveredContextFileWarnings) {
2242
+ logger.warn("Context file discovery warning", { warning });
2243
+ }
2236
2244
  const intentField = intentTracingEnabled ? INTENT_FIELD : undefined;
2237
2245
  const rebuildSystemPrompt = async (
2238
2246
  toolNames: string[],
@@ -2311,14 +2319,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2311
2319
  subagent: options.parentTaskPrefix !== undefined,
2312
2320
  });
2313
2321
 
2322
+ for (const warning of defaultPrompt.warnings) {
2323
+ if (emittedContextFileWarnings.has(warning)) continue;
2324
+ emittedContextFileWarnings.add(warning);
2325
+ contextFileWarnings.push(warning);
2326
+ if (hasSession) session.configWarnings.push(warning);
2327
+ logger.warn("Context file discovery warning", { warning });
2328
+ }
2314
2329
  if (options.systemPrompt === undefined) {
2315
2330
  return defaultPrompt;
2316
2331
  }
2317
2332
  if (Array.isArray(options.systemPrompt)) {
2318
- return { systemPrompt: options.systemPrompt };
2333
+ return { systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings };
2319
2334
  }
2320
2335
  return {
2321
2336
  systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
2337
+ warnings: defaultPrompt.warnings,
2322
2338
  };
2323
2339
  };
2324
2340
 
@@ -2797,6 +2813,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2797
2813
  forkContextSeed: options.forkContextSeed,
2798
2814
  providerSessionState: options.providerSessionState,
2799
2815
  });
2816
+ session.configWarnings.push(...contextFileWarnings);
2800
2817
  hasSession = true;
2801
2818
  if (asyncJobManager) {
2802
2819
  session.yieldQueue.register<AsyncResultEntry>("async-result", {