@alas-ide/codex-acp 2.5.0-alas.2 → 2.5.0-alas.4

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 (2) hide show
  1. package/dist/index.js +150 -37
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -33488,6 +33488,18 @@ function parsePatch(uniDiff) {
33488
33488
 
33489
33489
  // src/GitPatch.ts
33490
33490
  var DIFF_PATCH_MAX_BYTES = 1024 * 1024;
33491
+ var NEUTRAL_DIFF_PATCH_META_KEY = "diffPatch";
33492
+ var DIFF_PATCH_VERSION = 1;
33493
+ var DIFF_PATCH_FORMAT = "git_patch";
33494
+ function neutralDiffPatchCapability() {
33495
+ return { version: DIFF_PATCH_VERSION, format: DIFF_PATCH_FORMAT };
33496
+ }
33497
+ function clientRequestsNeutralDiffPatch(capabilities) {
33498
+ const declared = capabilities?._meta?.[NEUTRAL_DIFF_PATCH_META_KEY];
33499
+ if (declared === null || typeof declared !== "object" || Array.isArray(declared)) return false;
33500
+ const version2 = declared["version"];
33501
+ return typeof version2 === "number" && Number.isInteger(version2) && version2 >= 1;
33502
+ }
33491
33503
  var BINARY_PROBE_LENGTH = 8e3;
33492
33504
  var NO_NEWLINE_MARKER = "\";
33493
33505
  var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
@@ -33707,29 +33719,31 @@ var FileChangeReporter = class {
33707
33719
  }
33708
33720
  };
33709
33721
  function createPatchContent(change, diffPatch) {
33710
- const supportsDiffPatch = diffPatch === "air";
33722
+ const wrapper = diffPatch === "air" || diffPatch === "neutral" ? diffPatch : null;
33711
33723
  try {
33712
33724
  if (diffPatch === "acpV2") return createAcpV2Content(change);
33713
33725
  switch (change.kind.type) {
33714
33726
  case "add":
33715
- return createWholeFileContent(change, "add", supportsDiffPatch);
33727
+ return createWholeFileContent(change, "add", wrapper);
33716
33728
  case "delete":
33717
- return createWholeFileContent(change, "delete", supportsDiffPatch);
33729
+ return createWholeFileContent(change, "delete", wrapper);
33718
33730
  case "update":
33719
- return createUpdateFileContent(change, change.kind.move_path, supportsDiffPatch);
33731
+ return createUpdateFileContent(change, change.kind.move_path, wrapper);
33720
33732
  }
33721
33733
  } catch (error62) {
33722
33734
  logger.log(`Error processing file update change: ${error62}`);
33723
33735
  return [];
33724
33736
  }
33725
33737
  }
33726
- function createWholeFileContent(change, kind, supportsDiffPatch) {
33738
+ function createWholeFileContent(change, kind, wrapper) {
33727
33739
  if (!fitsDiffLimit(change.diff)) {
33728
33740
  logger.log("Skipped the diff of a file that is too large", { path: change.path });
33729
33741
  return [];
33730
33742
  }
33731
- const patch = !supportsDiffPatch ? null : kind === "add" ? createAddedFileGitPatch(change.path, change.diff) : createDeletedFileGitPatch(change.path, change.diff);
33732
- if (patch !== null) return [createPatchOnlyContent(change.path, kind, patch)];
33743
+ if (wrapper !== null) {
33744
+ const patch = kind === "add" ? createAddedFileGitPatch(change.path, change.diff) : createDeletedFileGitPatch(change.path, change.diff);
33745
+ if (patch !== null) return [createPatchOnlyContent(change.path, kind, patch, wrapper)];
33746
+ }
33733
33747
  return [{
33734
33748
  type: "diff",
33735
33749
  oldText: kind === "add" ? null : change.diff,
@@ -33738,16 +33752,16 @@ function createWholeFileContent(change, kind, supportsDiffPatch) {
33738
33752
  _meta: { kind }
33739
33753
  }];
33740
33754
  }
33741
- function createUpdateFileContent(change, movePath, supportsDiffPatch) {
33755
+ function createUpdateFileContent(change, movePath, wrapper) {
33742
33756
  const unifiedDiff = recoverCorruptedDiff(change.diff);
33743
33757
  const targetPath = movePath ?? change.path;
33744
33758
  if (!fitsDiffLimit(unifiedDiff)) {
33745
33759
  logger.log("Skipped the diff of a file change that is too large", { path: targetPath });
33746
33760
  return [];
33747
33761
  }
33748
- const gitPatch = supportsDiffPatch ? createUpdateGitPatch(change.path, targetPath, unifiedDiff) : null;
33749
- if (gitPatch !== null) {
33750
- return [createPatchOnlyContent(targetPath, "update", gitPatch)];
33762
+ if (wrapper !== null) {
33763
+ const gitPatch = createUpdateGitPatch(change.path, targetPath, unifiedDiff);
33764
+ if (gitPatch !== null) return [createPatchOnlyContent(targetPath, "update", gitPatch, wrapper)];
33751
33765
  }
33752
33766
  if (movePath !== null && unifiedDiff.trim().length === 0) {
33753
33767
  return [{ type: "diff", oldText: "", newText: "", path: targetPath, _meta: { kind: "update" } }];
@@ -33829,17 +33843,14 @@ function parseSinglePatch(diff) {
33829
33843
  return null;
33830
33844
  }
33831
33845
  }
33832
- function createPatchOnlyContent(path16, kind, patch) {
33846
+ function createPatchOnlyContent(path16, kind, patch, wrapper) {
33847
+ const diffPatch = { version: DIFF_PATCH_VERSION, format: DIFF_PATCH_FORMAT, text: patch };
33833
33848
  return {
33834
33849
  type: "diff",
33835
33850
  oldText: null,
33836
33851
  newText: "",
33837
33852
  path: path16,
33838
- _meta: withAirMeta({ kind }, AIR_DIFF_PATCH_KEY, {
33839
- version: 1,
33840
- format: "git_patch",
33841
- text: patch
33842
- })
33853
+ _meta: wrapper === "air" ? withAirMeta({ kind }, AIR_DIFF_PATCH_KEY, diffPatch) : { kind, [NEUTRAL_DIFF_PATCH_META_KEY]: diffPatch }
33843
33854
  };
33844
33855
  }
33845
33856
  function recoverCorruptedDiff(diff) {
@@ -34746,7 +34757,8 @@ var ClientCapabilities = class _ClientCapabilities {
34746
34757
  planContentDelta: clientSupportsAirCapability(capabilities, AIR_PLAN_CONTENT_DELTA_KEY),
34747
34758
  diffPatch
34748
34759
  },
34749
- diffPatchFormat: diffPatch ? "air" : "none",
34760
+ // A client that declares both forms gets only the client-neutral one.
34761
+ diffPatchFormat: clientRequestsNeutralDiffPatch(capabilities) ? "neutral" : diffPatch ? "air" : "none",
34750
34762
  permissionPromptFields: false
34751
34763
  });
34752
34764
  }
@@ -40611,7 +40623,7 @@ var package_default = {
40611
40623
  publishConfig: {
40612
40624
  access: "public"
40613
40625
  },
40614
- version: "2.5.0-alas.2",
40626
+ version: "2.5.0-alas.4",
40615
40627
  description: "",
40616
40628
  main: "dist/index.js",
40617
40629
  bin: {
@@ -40689,7 +40701,7 @@ var package_default = {
40689
40701
  alasDownstream: {
40690
40702
  upstreamVersion: "2.5.0",
40691
40703
  upstreamCommit: "34e08970aa324a2be985f5080674d5aa91d7666f",
40692
- sourceCommit: "0f50c0b0345fd0e67b91ffc3c49d6b998d27da0c"
40704
+ sourceCommit: "8af13bd4134b9a24acfd06b85418642f3c2742b6"
40693
40705
  }
40694
40706
  };
40695
40707
 
@@ -40919,9 +40931,31 @@ var CodexSubagentSubscriptions = class {
40919
40931
 
40920
40932
  // src/SessionFork.ts
40921
40933
  import { createHash } from "node:crypto";
40934
+ var NEUTRAL_FORK_POINT_META_KEY = "forkPoint";
40935
+ var NEUTRAL_FORK_POINT_VERSION = 1;
40936
+ function neutralForkPointCapability() {
40937
+ return { version: NEUTRAL_FORK_POINT_VERSION, boundary: "turn" };
40938
+ }
40939
+ function clientRequestsNeutralForkPoint(capabilities) {
40940
+ const declared = capabilities?._meta?.[NEUTRAL_FORK_POINT_META_KEY];
40941
+ if (!isUnknownRecord(declared)) return false;
40942
+ const version2 = declared["version"];
40943
+ return typeof version2 === "number" && Number.isInteger(version2) && version2 >= 1;
40944
+ }
40945
+ function hasNeutralForkPoint(meta3) {
40946
+ return meta3 != null && meta3[NEUTRAL_FORK_POINT_META_KEY] !== void 0;
40947
+ }
40948
+ function forkPointNotNegotiatedError() {
40949
+ return RequestError.invalidParams(
40950
+ { [NEUTRAL_FORK_POINT_META_KEY]: { reason: "notNegotiated" } },
40951
+ "session/fork _meta.forkPoint needs initialize clientCapabilities._meta.forkPoint"
40952
+ );
40953
+ }
40922
40954
  async function forkSession(request, additionalDirectories, dependencies, instructions) {
40955
+ const forkPoint = readForkPoint(request._meta);
40923
40956
  await dependencies.refreshSkills(request.cwd, additionalDirectories);
40924
- const lastTurnId = await resolveForkTurnId(request, dependencies.codexClient);
40957
+ const match = forkPoint ? await resolveForkPoint(request, forkPoint, dependencies.codexClient) : void 0;
40958
+ const lastTurnId = match?.turnId;
40925
40959
  const sessionConfig = await dependencies.createSessionConfig(
40926
40960
  request.cwd,
40927
40961
  additionalDirectories,
@@ -40953,26 +40987,83 @@ async function forkSession(request, additionalDirectories, dependencies, instruc
40953
40987
  currentServiceTier: response.serviceTier ?? null,
40954
40988
  additionalDirectories,
40955
40989
  skippedMcpServers: sessionConfig.skippedMcpServers,
40956
- ...instructions ? { instructions } : {}
40990
+ ...instructions ? { instructions } : {},
40991
+ ...forkPoint?.kind === "neutral" && match ? {
40992
+ forkPoint: {
40993
+ boundary: "turn",
40994
+ messageId: match.messageId,
40995
+ turnId: match.turnId,
40996
+ lastItemId: match.lastItemId,
40997
+ matchedBy: match.matchedBy
40998
+ }
40999
+ } : {}
40957
41000
  };
40958
41001
  }
40959
- async function resolveForkTurnId(request, codexClient) {
40960
- const forkPoint = readAirForkPoint(request._meta);
40961
- if (!forkPoint) return void 0;
41002
+ function readForkPoint(meta3) {
41003
+ if (hasNeutralForkPoint(meta3)) return { kind: "neutral", ...readNeutralForkPoint(meta3[NEUTRAL_FORK_POINT_META_KEY]) };
41004
+ const air = readAirForkPoint(meta3);
41005
+ return air ? { kind: "air", ...air } : void 0;
41006
+ }
41007
+ async function resolveForkPoint(request, forkPoint, codexClient) {
40962
41008
  const history = await codexClient.threadReadWithHistory(request.sessionId);
40963
- const candidateIds = airForkMessageIdCandidates(forkPoint.messageId);
40964
- const itemTurnId = candidateIds.map((candidateId) => history.thread.turns.find((turn) => turn.items.some((item) => item.id === candidateId))?.id).find((turnId) => turnId !== void 0);
40965
- if (itemTurnId) return itemTurnId;
41009
+ const turns = history.thread.turns;
41010
+ const matchById = (candidateId) => {
41011
+ const turn = turns.find((turn2) => turn2.items.some((item) => item.id === candidateId));
41012
+ return turn ? toMatch(turn, candidateId, "id") : void 0;
41013
+ };
41014
+ const candidateIds = forkPoint.kind === "air" ? airForkMessageIdCandidates(forkPoint.messageId) : [forkPoint.messageId];
41015
+ const idMatch = candidateIds.map(matchById).find((match) => match !== void 0);
41016
+ if (idMatch) return idMatch;
40966
41017
  if (forkPoint.messageFingerprint) {
40967
- const matchingTurns = history.thread.turns.flatMap((turn) => turn.items.filter((item) => item.type === "agentMessage" && fingerprintAgentMessage(item.text) === forkPoint.messageFingerprint).map(() => turn.id));
40968
- const fingerprintTurnId = matchingTurns[forkPoint.messageOccurrence - 1];
40969
- if (fingerprintTurnId) return fingerprintTurnId;
41018
+ const renderCwd = history.thread.cwd ?? request.cwd;
41019
+ const clientText = forkPoint.kind === "neutral" ? (text) => renderFileCitations(text, renderCwd) : (text) => text;
41020
+ const matches = turns.flatMap((turn) => turn.items.filter((item) => item.type === "agentMessage" && fingerprintAgentMessage(clientText(item.text)) === forkPoint.messageFingerprint).map((item) => toMatch(turn, item.id, "fingerprint")));
41021
+ const fingerprintMatch = matches[forkPoint.messageOccurrence - 1];
41022
+ if (fingerprintMatch) return fingerprintMatch;
41023
+ }
41024
+ if (forkPoint.kind === "neutral") {
41025
+ throw RequestError.invalidParams(
41026
+ { [NEUTRAL_FORK_POINT_META_KEY]: { messageId: forkPoint.messageId, reason: "notFound" } },
41027
+ `Fork point message ${forkPoint.messageId} was not found in session ${request.sessionId}`
41028
+ );
40970
41029
  }
40971
41030
  throw RequestError.invalidParams(
40972
41031
  { messageId: forkPoint.messageId },
40973
41032
  `Fork point message ${forkPoint.messageId} was not found in session ${request.sessionId}`
40974
41033
  );
40975
41034
  }
41035
+ function toMatch(turn, messageId, matchedBy) {
41036
+ return { messageId, turnId: turn.id, lastItemId: turn.items.at(-1).id, matchedBy };
41037
+ }
41038
+ function readNeutralForkPoint(forkPoint) {
41039
+ const invalid = (field, message) => RequestError.invalidParams(
41040
+ { [NEUTRAL_FORK_POINT_META_KEY]: { field } },
41041
+ `session/fork _meta.forkPoint: ${message}`
41042
+ );
41043
+ if (!isUnknownRecord(forkPoint)) throw invalid(NEUTRAL_FORK_POINT_META_KEY, "must be an object");
41044
+ const version2 = forkPoint["version"];
41045
+ if (version2 !== void 0 && version2 !== NEUTRAL_FORK_POINT_VERSION) {
41046
+ throw invalid("version", `version must be ${NEUTRAL_FORK_POINT_VERSION} when present`);
41047
+ }
41048
+ const messageId = forkPoint["messageId"];
41049
+ if (typeof messageId !== "string" || messageId.trim().length === 0) {
41050
+ throw invalid("messageId", "messageId must be a non-empty string");
41051
+ }
41052
+ const messageFingerprint = forkPoint["messageFingerprint"];
41053
+ if (messageFingerprint !== void 0 && (typeof messageFingerprint !== "string" || !SHA256_FINGERPRINT.test(messageFingerprint))) {
41054
+ throw invalid("messageFingerprint", "messageFingerprint must be sha256: and 64 lowercase hex digits");
41055
+ }
41056
+ const messageOccurrence = forkPoint["messageOccurrence"] ?? 1;
41057
+ if (!Number.isSafeInteger(messageOccurrence) || messageOccurrence < 1) {
41058
+ throw invalid("messageOccurrence", "messageOccurrence must be a positive integer");
41059
+ }
41060
+ return {
41061
+ messageId: messageId.trim(),
41062
+ ...typeof messageFingerprint === "string" && { messageFingerprint },
41063
+ messageOccurrence
41064
+ };
41065
+ }
41066
+ var SHA256_FINGERPRINT = /^sha256:[0-9a-f]{64}$/;
40976
41067
  function readAirForkPoint(meta3) {
40977
41068
  const jetbrains = meta3?.["jetbrains"];
40978
41069
  if (!isUnknownRecord(jetbrains)) return void 0;
@@ -40985,7 +41076,7 @@ function readAirForkPoint(meta3) {
40985
41076
  throw RequestError.invalidParams(void 0, "AIR fork messageId must be a non-empty string");
40986
41077
  }
40987
41078
  const messageFingerprint = fork["messageFingerprint"];
40988
- if (messageFingerprint !== void 0 && (typeof messageFingerprint !== "string" || !/^sha256:[0-9a-f]{64}$/.test(messageFingerprint))) {
41079
+ if (messageFingerprint !== void 0 && (typeof messageFingerprint !== "string" || !SHA256_FINGERPRINT.test(messageFingerprint))) {
40989
41080
  throw RequestError.invalidParams(void 0, "AIR fork messageFingerprint must be a SHA-256 fingerprint");
40990
41081
  }
40991
41082
  const messageOccurrence = fork["messageOccurrence"] ?? 1;
@@ -52061,6 +52152,8 @@ var CodexAcpServer = class {
52061
52152
  /** The capability choices of the client for tool call and plan reports. */
52062
52153
  capabilities;
52063
52154
  booleanConfigOptionsSupported;
52155
+ /** Whether the client opted into the client-neutral `session/fork` `_meta.forkPoint`. */
52156
+ neutralForkPointSupported = false;
52064
52157
  /** Last `authStatus` pushed to the client; used to suppress duplicates. */
52065
52158
  currentAuthStatus;
52066
52159
  /**
@@ -52323,6 +52416,7 @@ var CodexAcpServer = class {
52323
52416
  if (appServer) await appServer;
52324
52417
  this.reportingConnection.reports.compareMeta = this.capabilities.airToolCallContract;
52325
52418
  this.booleanConfigOptionsSupported = clientSupportsBooleanConfigOptions(_params.clientCapabilities);
52419
+ this.neutralForkPointSupported = clientRequestsNeutralForkPoint(_params.clientCapabilities);
52326
52420
  this.sessionIndex.configure(_params.clientCapabilities);
52327
52421
  this.hookReview.configure(_params.clientCapabilities?._meta?.["codexHooks"], this.capabilities.airClient);
52328
52422
  await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
@@ -52382,6 +52476,7 @@ var CodexAcpServer = class {
52382
52476
  if (appServer) await appServer;
52383
52477
  this.reportingConnection.reports.compareMeta = this.capabilities.airToolCallContract;
52384
52478
  this.booleanConfigOptionsSupported = true;
52479
+ this.neutralForkPointSupported = clientRequestsNeutralForkPoint(clientCapabilities);
52385
52480
  this.sessionIndex.configure(clientCapabilities, { neutral: false });
52386
52481
  await this.runWithProcessCheck(() => this.codexAcpClient.initialize({ clientInfo: params.info }));
52387
52482
  this.sessionIndex.observe(this.codexAcpClient);
@@ -52466,6 +52561,15 @@ var CodexAcpServer = class {
52466
52561
  "async-tasks": asyncTaskCapability()
52467
52562
  } : {},
52468
52563
  ...this.hookReview.enabled ? { codexHooks: { version: 1, reviewToken: true } } : {},
52564
+ // The client-neutral fork point, see `docs/air-extensions.md`.
52565
+ ...this.neutralForkPointSupported ? {
52566
+ [NEUTRAL_FORK_POINT_META_KEY]: neutralForkPointCapability()
52567
+ } : {},
52568
+ // The client-neutral diff patch, see `docs/air-extensions.md`. ACP v2 has its own diff patch,
52569
+ // so `ClientCapabilities.fromV2` never selects it.
52570
+ ...protocolVersion === 1 && this.capabilities.diffPatchFormat === "neutral" ? {
52571
+ [NEUTRAL_DIFF_PATCH_META_KEY]: neutralDiffPatchCapability()
52572
+ } : {},
52469
52573
  // The session index for a client that opted in without AIR, see `docs/air-extensions.md`.
52470
52574
  ...neutralSessionIndex ? {
52471
52575
  [NEUTRAL_SESSION_INDEX_META_KEY]: neutralSessionIndex
@@ -52759,7 +52863,7 @@ You have been logged out. Please try again.`);
52759
52863
  this.sessionGenerations.set(sessionId, generation);
52760
52864
  return generation;
52761
52865
  }
52762
- async tryCreateSession(request, operation = "sessionId" in request ? "resume" : "new") {
52866
+ async tryCreateSession(request, operation = "sessionId" in request ? "resume" : "new", onForked) {
52763
52867
  clientSystemPromptText(request.systemPrompt);
52764
52868
  const existingSessionRequest = request;
52765
52869
  const previousInstructions = "sessionId" in request ? this.sessions.get(request.sessionId)?.instructions : void 0;
@@ -52810,6 +52914,7 @@ You have been logged out. Please try again.`);
52810
52914
  previousInstructions
52811
52915
  );
52812
52916
  });
52917
+ onForked?.(sessionMetadata);
52813
52918
  } else {
52814
52919
  logger.log(`Create new session...`);
52815
52920
  sessionMetadata = await this.runWithProcessCheck(() => {
@@ -53378,6 +53483,9 @@ You have been logged out. Please try again.`);
53378
53483
  async forkSession(params) {
53379
53484
  params = this.withV1SessionInstructionAppend(params);
53380
53485
  clientSystemPromptText(params.systemPrompt);
53486
+ if (hasNeutralForkPoint(params._meta) && !this.neutralForkPointSupported) {
53487
+ throw forkPointNotNegotiatedError();
53488
+ }
53381
53489
  this.assertInstructionsReady(params.sessionId);
53382
53490
  return this.hookReview.run(
53383
53491
  params.cwd,
@@ -53393,12 +53501,16 @@ You have been logged out. Please try again.`);
53393
53501
  if (appServer) await appServer;
53394
53502
  logger.log("Forking session...", { sessionId: params.sessionId });
53395
53503
  try {
53396
- const [sessionId, , modeState] = await this.tryCreateSession(params, "fork");
53504
+ let forkPoint;
53505
+ const [sessionId, , modeState] = await this.tryCreateSession(params, "fork", (metadata) => {
53506
+ forkPoint = metadata.forkPoint;
53507
+ });
53397
53508
  logger.log("Session forked", { sourceSessionId: params.sessionId, sessionId });
53398
53509
  return {
53399
53510
  sessionId,
53400
53511
  modes: modeState,
53401
- ...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId))
53512
+ ...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId)),
53513
+ ...forkPoint ? { _meta: { [NEUTRAL_FORK_POINT_META_KEY]: forkPoint } } : {}
53402
53514
  };
53403
53515
  } catch (e) {
53404
53516
  const error62 = e instanceof Error ? e : new Error(String(e));
@@ -53409,10 +53521,11 @@ You have been logged out. Please try again.`);
53409
53521
  async forkSessionV2(params) {
53410
53522
  const { systemPrompt, ...rest } = params;
53411
53523
  const request = { ...rest, systemPrompt: toV1SystemPrompt(systemPrompt) };
53412
- const { sessionId } = await this.forkSession(request);
53524
+ const { sessionId, _meta } = await this.forkSession(request);
53413
53525
  return {
53414
53526
  sessionId,
53415
- ...this.createSessionSetupResponseV2(this.getSessionState(sessionId))
53527
+ ...this.createSessionSetupResponseV2(this.getSessionState(sessionId)),
53528
+ ..._meta ? { _meta } : {}
53416
53529
  };
53417
53530
  }
53418
53531
  async listSessions(params) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.5.0-alas.2",
6
+ "version": "2.5.0-alas.4",
7
7
  "description": "",
8
8
  "main": "dist/index.js",
9
9
  "bin": {
@@ -81,6 +81,6 @@
81
81
  "alasDownstream": {
82
82
  "upstreamVersion": "2.5.0",
83
83
  "upstreamCommit": "34e08970aa324a2be985f5080674d5aa91d7666f",
84
- "sourceCommit": "0f50c0b0345fd0e67b91ffc3c49d6b998d27da0c"
84
+ "sourceCommit": "8af13bd4134b9a24acfd06b85418642f3c2742b6"
85
85
  }
86
86
  }