arc-1 0.7.0 → 0.7.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.
Files changed (62) hide show
  1. package/README.md +11 -2
  2. package/dist/adt/client.d.ts +33 -19
  3. package/dist/adt/client.d.ts.map +1 -1
  4. package/dist/adt/client.js +84 -72
  5. package/dist/adt/client.js.map +1 -1
  6. package/dist/adt/discovery.d.ts +19 -2
  7. package/dist/adt/discovery.d.ts.map +1 -1
  8. package/dist/adt/discovery.js +17 -3
  9. package/dist/adt/discovery.js.map +1 -1
  10. package/dist/adt/features.d.ts +49 -0
  11. package/dist/adt/features.d.ts.map +1 -1
  12. package/dist/adt/features.js +109 -21
  13. package/dist/adt/features.js.map +1 -1
  14. package/dist/adt/types.d.ts +4 -0
  15. package/dist/adt/types.d.ts.map +1 -1
  16. package/dist/adt/xml-parser.d.ts +13 -5
  17. package/dist/adt/xml-parser.d.ts.map +1 -1
  18. package/dist/adt/xml-parser.js +55 -22
  19. package/dist/adt/xml-parser.js.map +1 -1
  20. package/dist/cache/cache.d.ts +11 -6
  21. package/dist/cache/cache.d.ts.map +1 -1
  22. package/dist/cache/cache.js +4 -4
  23. package/dist/cache/cache.js.map +1 -1
  24. package/dist/cache/caching-layer.d.ts +16 -4
  25. package/dist/cache/caching-layer.d.ts.map +1 -1
  26. package/dist/cache/caching-layer.js +40 -13
  27. package/dist/cache/caching-layer.js.map +1 -1
  28. package/dist/cache/inactive-list-cache.d.ts +15 -0
  29. package/dist/cache/inactive-list-cache.d.ts.map +1 -0
  30. package/dist/cache/inactive-list-cache.js +37 -0
  31. package/dist/cache/inactive-list-cache.js.map +1 -0
  32. package/dist/cache/memory.d.ts +6 -3
  33. package/dist/cache/memory.d.ts.map +1 -1
  34. package/dist/cache/memory.js +14 -6
  35. package/dist/cache/memory.js.map +1 -1
  36. package/dist/cache/sqlite.d.ts +7 -3
  37. package/dist/cache/sqlite.d.ts.map +1 -1
  38. package/dist/cache/sqlite.js +32 -9
  39. package/dist/cache/sqlite.js.map +1 -1
  40. package/dist/cache/warmup.js +7 -3
  41. package/dist/cache/warmup.js.map +1 -1
  42. package/dist/cli.js +4 -1
  43. package/dist/cli.js.map +1 -1
  44. package/dist/context/compressor.d.ts.map +1 -1
  45. package/dist/context/compressor.js +15 -12
  46. package/dist/context/compressor.js.map +1 -1
  47. package/dist/handlers/hyperfocused.d.ts.map +1 -1
  48. package/dist/handlers/hyperfocused.js +7 -0
  49. package/dist/handlers/hyperfocused.js.map +1 -1
  50. package/dist/handlers/intent.d.ts.map +1 -1
  51. package/dist/handlers/intent.js +161 -87
  52. package/dist/handlers/intent.js.map +1 -1
  53. package/dist/handlers/schemas.d.ts +12 -0
  54. package/dist/handlers/schemas.d.ts.map +1 -1
  55. package/dist/handlers/schemas.js +4 -0
  56. package/dist/handlers/schemas.js.map +1 -1
  57. package/dist/handlers/tools.d.ts.map +1 -1
  58. package/dist/handlers/tools.js +13 -2
  59. package/dist/handlers/tools.js.map +1 -1
  60. package/dist/server/server.d.ts +1 -1
  61. package/dist/server/server.js +1 -1
  62. package/package.json +3 -1
@@ -834,7 +834,7 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
834
834
  result = await handleSAPWrite(client, args, config, cachingLayer);
835
835
  break;
836
836
  case 'SAPActivate':
837
- result = await handleSAPActivate(client, args);
837
+ result = await handleSAPActivate(client, args, cachingLayer);
838
838
  break;
839
839
  case 'SAPNavigate':
840
840
  result = await handleSAPNavigate(client, args);
@@ -928,23 +928,92 @@ const BTP_HINTS = {
928
928
  SOBJ: 'BOR business objects (SOBJ) are not available on BTP ABAP Environment. Use RAP behavior definitions (BDEF) instead.',
929
929
  TRAN: 'Transaction codes (TRAN) are not available on BTP ABAP Environment. Use SAPSearch to find apps and services instead.',
930
930
  };
931
+ const VERSIONED_SOURCE_READ_TYPES = new Set([
932
+ 'PROG',
933
+ 'CLAS',
934
+ 'INTF',
935
+ 'FUNC',
936
+ 'INCL',
937
+ 'DDLS',
938
+ 'DCLS',
939
+ 'BDEF',
940
+ 'SRVD',
941
+ 'DDLX',
942
+ 'SRVB',
943
+ 'SKTD',
944
+ 'TABL',
945
+ 'VIEW',
946
+ 'STRU',
947
+ ]);
948
+ function inactiveTypeMatches(readType, inactiveType) {
949
+ return (inactiveType.split('/')[0] ?? inactiveType).toUpperCase() === readType.toUpperCase();
950
+ }
951
+ async function resolveVersionAndDraftInfo(client, cachingLayer, type, name, requestedVersion) {
952
+ if (!VERSIONED_SOURCE_READ_TYPES.has(type)) {
953
+ return { effectiveVersion: requestedVersion === 'auto' ? 'active' : requestedVersion };
954
+ }
955
+ let draft;
956
+ if (cachingLayer || requestedVersion !== 'active') {
957
+ try {
958
+ const inactiveObjects = cachingLayer
959
+ ? await cachingLayer.inactiveLists.getOrFetch(client)
960
+ : await client.getInactiveObjects();
961
+ const upperName = name.toUpperCase();
962
+ draft = inactiveObjects.find((object) => inactiveTypeMatches(type, object.type) && object.name.toUpperCase() === upperName);
963
+ }
964
+ catch (err) {
965
+ logger.debug('Inactive object list unavailable while resolving source version', {
966
+ type,
967
+ name,
968
+ requestedVersion,
969
+ error: err instanceof Error ? err.message : String(err),
970
+ });
971
+ }
972
+ }
973
+ if (requestedVersion === 'auto') {
974
+ return { effectiveVersion: draft ? 'inactive' : 'active', draft };
975
+ }
976
+ return { effectiveVersion: requestedVersion, draft };
977
+ }
978
+ function sourceVersionWarning(effectiveVersion, draft) {
979
+ if (effectiveVersion === 'active' && draft) {
980
+ const deletion = draft.deleted ? ' deletion' : '';
981
+ const transport = draft.transport ? ` (in transport ${draft.transport})` : '';
982
+ return `Note: You have an unactivated${deletion} draft of this object${transport}. The source below is the LAST ACTIVATED version. To work with your draft, activate it first via SAPActivate or re-run with version='inactive' to read it directly.`;
983
+ }
984
+ if (effectiveVersion === 'inactive' && !draft) {
985
+ return 'Note: No inactive draft exists for this object on the server. Returning the active version.';
986
+ }
987
+ return undefined;
988
+ }
931
989
  async function handleSAPRead(client, args, cachingLayer) {
932
990
  const type = normalizeObjectType(String(args.type ?? ''));
933
991
  const name = String(args.name ?? '');
992
+ const requestedVersion = (args.version ?? 'active');
934
993
  // BTP: return helpful error for unavailable types
935
994
  if (isBtpSystem() && BTP_HINTS[type]) {
936
995
  return errorResult(BTP_HINTS[type]);
937
996
  }
997
+ if (args.force_refresh === true && cachingLayer && VERSIONED_SOURCE_READ_TYPES.has(type)) {
998
+ cachingLayer.inactiveLists.invalidate(client.username);
999
+ cachingLayer.invalidate(type, name, 'all');
1000
+ }
1001
+ const { effectiveVersion, draft } = await resolveVersionAndDraftInfo(client, cachingLayer, type, name, requestedVersion);
1002
+ const versionWarning = sourceVersionWarning(effectiveVersion, draft);
938
1003
  // Helper: get source with cache support, returns cache hit status
939
- const cachedGet = async (objType, objName, fetcher) => {
940
- if (!cachingLayer)
941
- return { source: await fetcher(), cacheHit: false };
942
- const { source, hit } = await cachingLayer.getSource(objType, objName, fetcher);
943
- return { source, cacheHit: hit };
1004
+ const cachedGet = async (objType, objName, version, fetcher) => {
1005
+ if (!cachingLayer) {
1006
+ const result = await fetcher(undefined);
1007
+ return { source: result.source, cacheHit: false, revalidated: false };
1008
+ }
1009
+ const { source, hit, revalidated } = await cachingLayer.getSource(objType, objName, fetcher, { version });
1010
+ return { source, cacheHit: hit, revalidated };
944
1011
  };
945
- /** Prepend [cached] indicator when result came from cache */
946
- const cachedTextResult = (source, cacheHit) => {
947
- return textResult(cacheHit ? `[cached]\n${source}` : source);
1012
+ /** Prepend draft-awareness notes and cache indicator when the server revalidated a cached source. */
1013
+ const cachedTextResult = (source, cacheHit, revalidated, warning) => {
1014
+ const note = warning ? `${warning}\n\n` : '';
1015
+ const indicator = cacheHit && revalidated ? '[cached:revalidated]\n' : '';
1016
+ return textResult(`${note}${indicator}${source}`);
948
1017
  };
949
1018
  // Structured format is only supported for CLAS type
950
1019
  if (args.format === 'structured' && type !== 'CLAS') {
@@ -952,8 +1021,8 @@ async function handleSAPRead(client, args, cachingLayer) {
952
1021
  }
953
1022
  switch (type) {
954
1023
  case 'PROG': {
955
- const { source, cacheHit } = await cachedGet('PROG', name, () => client.getProgram(name));
956
- return cachedTextResult(source, cacheHit);
1024
+ const { source, cacheHit, revalidated } = await cachedGet('PROG', name, effectiveVersion, (ifNoneMatch) => client.getProgram(name, { ifNoneMatch, version: effectiveVersion }));
1025
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
957
1026
  }
958
1027
  case 'CLAS': {
959
1028
  // Structured format: return JSON with metadata + decomposed source
@@ -964,7 +1033,7 @@ async function handleSAPRead(client, args, cachingLayer) {
964
1033
  const methodParam = args.method;
965
1034
  if (methodParam && !args.include) {
966
1035
  // Method-level read — fetch full source then extract (no cache indicator for derived results)
967
- const { source: fullSource } = await cachedGet('CLAS', name, () => client.getClass(name));
1036
+ const { source: fullSource } = await cachedGet('CLAS', name, effectiveVersion, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch, version: effectiveVersion }));
968
1037
  const abaplintVer = cachedFeatures?.abapRelease
969
1038
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
970
1039
  : undefined;
@@ -976,18 +1045,21 @@ async function handleSAPRead(client, args, cachingLayer) {
976
1045
  if (!extracted.success) {
977
1046
  return errorResult(extracted.error ?? `Method "${methodParam}" not found in ${name}.`);
978
1047
  }
979
- return textResult(extracted.methodSource);
1048
+ return cachedTextResult(extracted.methodSource, false, false, versionWarning);
980
1049
  }
981
1050
  // Only cache the full merged source (no include param), not individual includes
982
1051
  if (!args.include) {
983
- const { source, cacheHit } = await cachedGet('CLAS', name, () => client.getClass(name));
984
- return cachedTextResult(source, cacheHit);
1052
+ const { source, cacheHit, revalidated } = await cachedGet('CLAS', name, effectiveVersion, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch, version: effectiveVersion }));
1053
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
985
1054
  }
986
- return textResult(await client.getClass(name, args.include));
1055
+ const includeResult = await client.getClass(name, args.include, {
1056
+ version: effectiveVersion,
1057
+ });
1058
+ return cachedTextResult(includeResult.source, false, false, versionWarning);
987
1059
  }
988
1060
  case 'INTF': {
989
- const { source, cacheHit } = await cachedGet('INTF', name, () => client.getInterface(name));
990
- return cachedTextResult(source, cacheHit);
1061
+ const { source, cacheHit, revalidated } = await cachedGet('INTF', name, effectiveVersion, (ifNoneMatch) => client.getInterface(name, { ifNoneMatch, version: effectiveVersion }));
1062
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
991
1063
  }
992
1064
  case 'FUNC': {
993
1065
  let group = String(args.group ?? '');
@@ -1001,13 +1073,13 @@ async function handleSAPRead(client, args, cachingLayer) {
1001
1073
  }
1002
1074
  group = resolved;
1003
1075
  }
1004
- const { source, cacheHit } = await cachedGet('FUNC', name, () => client.getFunction(group, name));
1005
- return cachedTextResult(source, cacheHit);
1076
+ const { source, cacheHit, revalidated } = await cachedGet('FUNC', name, effectiveVersion, (ifNoneMatch) => client.getFunction(group, name, { ifNoneMatch, version: effectiveVersion }));
1077
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1006
1078
  }
1007
1079
  case 'FUGR': {
1008
1080
  const expand = Boolean(args.expand_includes);
1009
1081
  if (expand) {
1010
- const source = await client.getFunctionGroupSource(name);
1082
+ const { source } = await client.getFunctionGroupSource(name, { version: effectiveVersion });
1011
1083
  // Match INCLUDE statements but skip ABAP comment lines (starting with *)
1012
1084
  const includePattern = /^[^*\n]*\bINCLUDE\s+(\S+)\s*\./gim;
1013
1085
  const parts = [`=== FUGR ${name} (main) ===\n${source}`];
@@ -1015,7 +1087,7 @@ async function handleSAPRead(client, args, cachingLayer) {
1015
1087
  while ((m = includePattern.exec(source)) !== null) {
1016
1088
  const inclName = m[1];
1017
1089
  try {
1018
- const inclSource = await client.getInclude(inclName);
1090
+ const { source: inclSource } = await client.getInclude(inclName, { version: effectiveVersion });
1019
1091
  parts.push(`\n=== ${inclName} ===\n${inclSource}`);
1020
1092
  }
1021
1093
  catch {
@@ -1028,37 +1100,37 @@ async function handleSAPRead(client, args, cachingLayer) {
1028
1100
  return textResult(JSON.stringify(fg, null, 2));
1029
1101
  }
1030
1102
  case 'INCL': {
1031
- const { source, cacheHit } = await cachedGet('INCL', name, () => client.getInclude(name));
1032
- return cachedTextResult(source, cacheHit);
1103
+ const { source, cacheHit, revalidated } = await cachedGet('INCL', name, effectiveVersion, (ifNoneMatch) => client.getInclude(name, { ifNoneMatch, version: effectiveVersion }));
1104
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1033
1105
  }
1034
1106
  case 'DDLS': {
1035
- const { source: ddlSource, cacheHit } = await cachedGet('DDLS', name, () => client.getDdls(name));
1107
+ const { source: ddlSource, cacheHit, revalidated, } = await cachedGet('DDLS', name, effectiveVersion, (ifNoneMatch) => client.getDdls(name, { ifNoneMatch, version: effectiveVersion }));
1036
1108
  if (ddlSource.trim() === '') {
1037
1109
  return textResult(`DDLS ${name} exists in the object directory but has no source code stored. ` +
1038
1110
  `The DDL source may need to be written via SAPWrite(action="create" or "update", type="DDLS", name="${name}", source="...").`);
1039
1111
  }
1040
1112
  if (args.include?.toLowerCase() === 'elements') {
1041
1113
  // Elements extraction is derived from source — no cache indicator
1042
- return textResult(extractCdsElements(ddlSource, name));
1114
+ return cachedTextResult(extractCdsElements(ddlSource, name), false, false, versionWarning);
1043
1115
  }
1044
- return cachedTextResult(ddlSource, cacheHit);
1116
+ return cachedTextResult(ddlSource, cacheHit, revalidated, versionWarning);
1045
1117
  }
1046
1118
  case 'DCLS': {
1047
- const { source, cacheHit } = await cachedGet('DCLS', name, () => client.getDcl(name));
1048
- return cachedTextResult(source, cacheHit);
1119
+ const { source, cacheHit, revalidated } = await cachedGet('DCLS', name, effectiveVersion, (ifNoneMatch) => client.getDcl(name, { ifNoneMatch, version: effectiveVersion }));
1120
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1049
1121
  }
1050
1122
  case 'BDEF': {
1051
- const { source, cacheHit } = await cachedGet('BDEF', name, () => client.getBdef(name));
1052
- return cachedTextResult(source, cacheHit);
1123
+ const { source, cacheHit, revalidated } = await cachedGet('BDEF', name, effectiveVersion, (ifNoneMatch) => client.getBdef(name, { ifNoneMatch, version: effectiveVersion }));
1124
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1053
1125
  }
1054
1126
  case 'SRVD': {
1055
- const { source, cacheHit } = await cachedGet('SRVD', name, () => client.getSrvd(name));
1056
- return cachedTextResult(source, cacheHit);
1127
+ const { source, cacheHit, revalidated } = await cachedGet('SRVD', name, effectiveVersion, (ifNoneMatch) => client.getSrvd(name, { ifNoneMatch, version: effectiveVersion }));
1128
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1057
1129
  }
1058
1130
  case 'DDLX': {
1059
1131
  try {
1060
- const { source, cacheHit } = await cachedGet('DDLX', name, () => client.getDdlx(name));
1061
- return cachedTextResult(source, cacheHit);
1132
+ const { source, cacheHit, revalidated } = await cachedGet('DDLX', name, effectiveVersion, (ifNoneMatch) => client.getDdlx(name, { ifNoneMatch, version: effectiveVersion }));
1133
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1062
1134
  }
1063
1135
  catch (err) {
1064
1136
  if (isNotFoundError(err)) {
@@ -1068,16 +1140,16 @@ async function handleSAPRead(client, args, cachingLayer) {
1068
1140
  }
1069
1141
  }
1070
1142
  case 'SRVB': {
1071
- const { source, cacheHit } = await cachedGet('SRVB', name, () => client.getSrvb(name));
1072
- return cachedTextResult(source, cacheHit);
1143
+ const { source, cacheHit, revalidated } = await cachedGet('SRVB', name, effectiveVersion, (ifNoneMatch) => client.getSrvb(name, { ifNoneMatch, version: effectiveVersion }));
1144
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1073
1145
  }
1074
1146
  case 'SKTD': {
1075
1147
  try {
1076
1148
  // ADT returns a <sktd:docu> XML envelope with the Markdown body base64-encoded
1077
1149
  // inside <sktd:text>. Cache the raw envelope (update flow re-uses it) and
1078
1150
  // return the decoded Markdown to the LLM.
1079
- const { source, cacheHit } = await cachedGet('SKTD', name, () => client.getKtd(name));
1080
- return cachedTextResult(decodeKtdText(source), cacheHit);
1151
+ const { source, cacheHit, revalidated } = await cachedGet('SKTD', name, effectiveVersion, (ifNoneMatch) => client.getKtd(name, { ifNoneMatch, version: effectiveVersion }));
1152
+ return cachedTextResult(decodeKtdText(source), cacheHit, revalidated, versionWarning);
1081
1153
  }
1082
1154
  catch (err) {
1083
1155
  if (isNotFoundError(err)) {
@@ -1087,16 +1159,16 @@ async function handleSAPRead(client, args, cachingLayer) {
1087
1159
  }
1088
1160
  }
1089
1161
  case 'TABL': {
1090
- const { source, cacheHit } = await cachedGet('TABL', name, () => client.getTable(name));
1091
- return cachedTextResult(source, cacheHit);
1162
+ const { source, cacheHit, revalidated } = await cachedGet('TABL', name, effectiveVersion, (ifNoneMatch) => client.getTable(name, { ifNoneMatch, version: effectiveVersion }));
1163
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1092
1164
  }
1093
1165
  case 'VIEW': {
1094
- const { source, cacheHit } = await cachedGet('VIEW', name, () => client.getView(name));
1095
- return cachedTextResult(source, cacheHit);
1166
+ const { source, cacheHit, revalidated } = await cachedGet('VIEW', name, effectiveVersion, (ifNoneMatch) => client.getView(name, { ifNoneMatch, version: effectiveVersion }));
1167
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1096
1168
  }
1097
1169
  case 'STRU': {
1098
- const { source, cacheHit } = await cachedGet('STRU', name, () => client.getStructure(name));
1099
- return cachedTextResult(source, cacheHit);
1170
+ const { source, cacheHit, revalidated } = await cachedGet('STRU', name, effectiveVersion, (ifNoneMatch) => client.getStructure(name, { ifNoneMatch, version: effectiveVersion }));
1171
+ return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1100
1172
  }
1101
1173
  case 'DOMA': {
1102
1174
  const domain = await client.getDomain(name);
@@ -1208,7 +1280,7 @@ async function handleSAPRead(client, args, cachingLayer) {
1208
1280
  if (!prog) {
1209
1281
  return errorResult(`BOR method "${method}" on "${name}" has no program assigned.`);
1210
1282
  }
1211
- const source = await client.getProgram(prog);
1283
+ const { source } = await client.getProgram(prog);
1212
1284
  return textResult(`=== BOR ${name}.${method} (program: ${prog}, form: ${String(data.rows[0].FORMNAME ?? '').trim()}) ===\n${source}`);
1213
1285
  }
1214
1286
  return errorResult(`BOR method "${method}" not found on object type "${name}". Use SAPRead(type="SOBJ", name="${name}") without method to list all methods.`);
@@ -1280,18 +1352,8 @@ async function handleSAPRead(client, args, cachingLayer) {
1280
1352
  return textResult(JSON.stringify(info, null, 2));
1281
1353
  }
1282
1354
  case 'INACTIVE_OBJECTS': {
1283
- try {
1284
- const objects = await client.getInactiveObjects();
1285
- return textResult(JSON.stringify({ count: objects.length, objects }, null, 2));
1286
- }
1287
- catch (err) {
1288
- if (isNotFoundError(err)) {
1289
- return textResult('Inactive objects listing is not available on this SAP system ' +
1290
- '(the /sap/bc/adt/activation/inactive endpoint returned 404). ' +
1291
- 'Use SAPDiagnose(action="syntax", type="...", name="...") to check specific objects instead.');
1292
- }
1293
- throw err;
1294
- }
1355
+ const objects = await client.getInactiveObjects();
1356
+ return textResult(JSON.stringify({ count: objects.length, objects }, null, 2));
1295
1357
  }
1296
1358
  default:
1297
1359
  return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DCLS, DDLX, BDEF, SRVD, SRVB, SKTD, TABL, VIEW, STRU, DOMA, DTEL, AUTH, FTG2, ENHO, VERSIONS, VERSION_SOURCE, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE, INACTIVE_OBJECTS. ` +
@@ -1658,7 +1720,7 @@ async function mergeMetadataWriteProperties(client, type, name, provided) {
1658
1720
  };
1659
1721
  }
1660
1722
  if (type === 'SRVB') {
1661
- const existingRaw = await client.getSrvb(name);
1723
+ const { source: existingRaw } = await client.getSrvb(name);
1662
1724
  const existing = JSON.parse(existingRaw);
1663
1725
  return {
1664
1726
  _description: existing.description,
@@ -2195,6 +2257,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2195
2257
  }
2196
2258
  const objectUrl = objectUrlForType(type, name);
2197
2259
  const srcUrl = sourceUrlForType(type, name);
2260
+ const invalidateWrittenObject = (objType = type, objName = name) => {
2261
+ cachingLayer?.invalidate(objType, objName, 'all');
2262
+ cachingLayer?.inactiveLists.invalidate(client.username);
2263
+ };
2198
2264
  // Helper: enforce allowedPackages for existing objects (update/delete/edit_method/scaffold_rap_handlers).
2199
2265
  // Only fetches metadata when package restrictions are configured — no extra HTTP call otherwise.
2200
2266
  async function enforcePackageForExistingObject() {
@@ -2215,10 +2281,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2215
2281
  // no-ops (or 415s on strict systems). Fetch the current envelope,
2216
2282
  // replace only the <sktd:text> body, and PUT it back — preserves
2217
2283
  // responsible/masterLanguage/packageRef/refObject metadata.
2218
- const currentEnvelope = await client.getKtd(name);
2284
+ const { source: currentEnvelope } = await client.getKtd(name);
2219
2285
  const body = rewriteKtdText(currentEnvelope, source);
2220
2286
  await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, transport);
2221
- cachingLayer?.invalidate(type, name);
2287
+ invalidateWrittenObject();
2222
2288
  return textResult(`Successfully updated ${type} ${name}.`);
2223
2289
  }
2224
2290
  if (isMetadataWriteType(type)) {
@@ -2231,7 +2297,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2231
2297
  const pkg = String(args.package ?? existingPackage ?? mergedProps._package ?? '$TMP');
2232
2298
  const body = buildCreateXml(type, name, pkg, description, mergedProps);
2233
2299
  await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport);
2234
- cachingLayer?.invalidate(type, name);
2300
+ invalidateWrittenObject();
2235
2301
  return textResult(`Successfully updated ${type} ${name}.`);
2236
2302
  }
2237
2303
  // RAP deterministic preflight validation
@@ -2251,7 +2317,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2251
2317
  // If safeUpdateSource throws (lock conflict, network error, etc.), checkNotes
2252
2318
  // is intentionally discarded — pre-check warnings only matter when the write succeeded.
2253
2319
  await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
2254
- cachingLayer?.invalidate(type, name);
2320
+ invalidateWrittenObject();
2255
2321
  const msg = `Successfully updated ${type} ${name}.`;
2256
2322
  const cdsUpdateHint = type === 'DDLS' ? await buildCdsUpdateCrudHint(client, name, objectUrl) : undefined;
2257
2323
  const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint);
@@ -2338,13 +2404,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2338
2404
  // PUT back exactly the shape SAP gave us (with all the server-assigned
2339
2405
  // metadata), only swapping <sktd:text>.
2340
2406
  if (source) {
2341
- const currentEnvelope = await client.getKtd(name);
2407
+ const { source: currentEnvelope } = await client.getKtd(name);
2342
2408
  const body = rewriteKtdText(currentEnvelope, source);
2343
2409
  await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, effectiveTransport);
2344
- cachingLayer?.invalidate(type, name);
2410
+ invalidateWrittenObject();
2345
2411
  return textResult(`Created SKTD ${name} in package ${pkg} and wrote Markdown content.\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
2346
2412
  }
2347
- cachingLayer?.invalidate(type, name);
2413
+ invalidateWrittenObject();
2348
2414
  return textResult(`Created SKTD ${name} in package ${pkg} (no Markdown content written — pass "source" to write the body).\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
2349
2415
  }
2350
2416
  // Build type-specific creation XML body.
@@ -2403,7 +2469,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2403
2469
  }
2404
2470
  });
2405
2471
  }
2406
- cachingLayer?.invalidate(type, name);
2472
+ invalidateWrittenObject();
2407
2473
  const followUpHint = type === 'SRVB'
2408
2474
  ? `\n\nNext steps:\n1. SAPActivate(type="SRVB", name="${name}")\n2. SAPActivate(action="publish_srvb", name="${name}")`
2409
2475
  : '';
@@ -2417,7 +2483,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2417
2483
  return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
2418
2484
  }
2419
2485
  await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport);
2420
- cachingLayer?.invalidate(type, name);
2486
+ invalidateWrittenObject();
2421
2487
  const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
2422
2488
  const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings);
2423
2489
  return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
@@ -2435,8 +2501,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2435
2501
  await enforcePackageForExistingObject();
2436
2502
  // Fetch current full source (use cache if available)
2437
2503
  const currentSource = cachingLayer
2438
- ? (await cachingLayer.getSource('CLAS', name, () => client.getClass(name))).source
2439
- : await client.getClass(name);
2504
+ ? (await cachingLayer.getSource('CLAS', name, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch }))).source
2505
+ : (await client.getClass(name)).source;
2440
2506
  // Use detected ABAP version from probe if available
2441
2507
  const abaplintVer = cachedFeatures?.abapRelease
2442
2508
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
@@ -2454,7 +2520,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2454
2520
  const checkNotes = await runPreWriteSyntaxCheck(client, type, spliced.newSource, objectUrl, config, checkOverride);
2455
2521
  // Write the full source back (existing lock/modify/unlock flow)
2456
2522
  await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport);
2457
- cachingLayer?.invalidate(type, name);
2523
+ invalidateWrittenObject();
2458
2524
  const msg = `Successfully updated method "${method}" in ${type} ${name}.`;
2459
2525
  const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
2460
2526
  return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
@@ -2510,8 +2576,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2510
2576
  .filter(Boolean)
2511
2577
  .join('\n\n');
2512
2578
  const bdefSource = cachingLayer
2513
- ? (await cachingLayer.getSource('BDEF', bdefName, () => client.getBdef(bdefName))).source
2514
- : await client.getBdef(bdefName);
2579
+ ? (await cachingLayer.getSource('BDEF', bdefName, (ifNoneMatch) => client.getBdef(bdefName, { ifNoneMatch })))
2580
+ .source
2581
+ : (await client.getBdef(bdefName)).source;
2515
2582
  let requirements = extractRapHandlerRequirements(bdefSource);
2516
2583
  if (targetAlias) {
2517
2584
  requirements = requirements.filter((req) => req.entityAlias.toLowerCase() === targetAlias);
@@ -2622,7 +2689,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2622
2689
  }
2623
2690
  }
2624
2691
  });
2625
- cachingLayer?.invalidate(type, name);
2692
+ invalidateWrittenObject();
2626
2693
  const msg = `Scaffolded ${scaffoldPlan.insertedSignatureCount} RAP handler signature(s) and ${scaffoldPlan.insertedImplementationStubCount} implementation stub(s) in ${type} ${name} from BDEF ${bdefName}. ` +
2627
2694
  `Updated section(s): ${scaffoldPlan.changedSections.join(', ')}.`;
2628
2695
  const warnings = mergePreWriteWarnings(lintWarningsMain?.warnings, lintWarningsDefinitions?.warnings, lintWarningsImplementations?.warnings);
@@ -2672,7 +2739,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2672
2739
  }
2673
2740
  throw err;
2674
2741
  }
2675
- cachingLayer?.invalidate(type, name);
2742
+ invalidateWrittenObject();
2676
2743
  return textResult(`Deleted ${type} ${name}.`);
2677
2744
  }
2678
2745
  case 'batch_create': {
@@ -2809,7 +2876,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2809
2876
  });
2810
2877
  break;
2811
2878
  }
2812
- cachingLayer?.invalidate(objType, objName);
2879
+ invalidateWrittenObject(objType, objName);
2813
2880
  results.push({ type: objType, name: objName, status: 'success' });
2814
2881
  }
2815
2882
  catch (err) {
@@ -3002,7 +3069,7 @@ async function runPreWriteSyntaxCheck(client, type, source, objectUrl, config, p
3002
3069
  }
3003
3070
  }
3004
3071
  // ─── SAPActivate Handler ─────────────────────────────────────────────
3005
- async function handleSAPActivate(client, args) {
3072
+ async function handleSAPActivate(client, args, cachingLayer) {
3006
3073
  const action = String(args.action ?? 'activate');
3007
3074
  const name = String(args.name ?? '');
3008
3075
  const version = String(args.version ?? '0001');
@@ -3013,7 +3080,7 @@ async function handleSAPActivate(client, args) {
3013
3080
  if (explicitServiceType === 'odatav4' || explicitServiceType === 'odatav2')
3014
3081
  return explicitServiceType;
3015
3082
  try {
3016
- const srvbJson = await client.getSrvb(name);
3083
+ const { source: srvbJson } = await client.getSrvb(name);
3017
3084
  const srvb = JSON.parse(srvbJson);
3018
3085
  if (srvb.odataVersion === 'V4')
3019
3086
  return 'odatav4';
@@ -3035,7 +3102,7 @@ async function handleSAPActivate(client, args) {
3035
3102
  }
3036
3103
  let srvbInfo;
3037
3104
  try {
3038
- srvbInfo = await client.getSrvb(name);
3105
+ srvbInfo = (await client.getSrvb(name)).source;
3039
3106
  }
3040
3107
  catch {
3041
3108
  if (result.severity === 'UNKNOWN') {
@@ -3070,7 +3137,7 @@ async function handleSAPActivate(client, args) {
3070
3137
  }
3071
3138
  let srvbInfo;
3072
3139
  try {
3073
- srvbInfo = await client.getSrvb(name);
3140
+ srvbInfo = (await client.getSrvb(name)).source;
3074
3141
  }
3075
3142
  catch {
3076
3143
  // Readback failed — fall through with what we have
@@ -3108,6 +3175,10 @@ async function handleSAPActivate(client, args) {
3108
3175
  const batchStatuses = buildBatchActivationStatuses(objects, result);
3109
3176
  const statusDetails = formatBatchActivationStatuses(batchStatuses);
3110
3177
  if (result.success) {
3178
+ for (const object of objects) {
3179
+ cachingLayer?.invalidate(object.type, object.name, 'all');
3180
+ }
3181
+ cachingLayer?.inactiveLists.invalidate(client.username);
3111
3182
  return textResult(`Successfully activated ${objects.length} objects: ${names}.${statusDetails}`);
3112
3183
  }
3113
3184
  // On batch failure enrich with per-object inactive-version syntax errors —
@@ -3124,6 +3195,8 @@ async function handleSAPActivate(client, args) {
3124
3195
  const objectUrl = objectUrlForType(type, name);
3125
3196
  const result = await activate(client.http, client.safety, objectUrl, { ...activateOpts, name });
3126
3197
  if (result.success) {
3198
+ cachingLayer?.invalidate(type, name, 'all');
3199
+ cachingLayer?.inactiveLists.invalidate(client.username);
3127
3200
  return textResult(`Successfully activated ${type} ${name}.${formatActivationMessages(result)}`);
3128
3201
  }
3129
3202
  // On failure, try to enrich with the actual compiler errors from the inactive version —
@@ -3973,7 +4046,7 @@ async function handleSAPContext(client, args, cachingLayer) {
3973
4046
  // Helper: get source with cache support
3974
4047
  const cachedGet = async (objType, objName, fetcher) => {
3975
4048
  if (!cachingLayer)
3976
- return fetcher();
4049
+ return (await fetcher()).source;
3977
4050
  const { source } = await cachingLayer.getSource(objType, objName, fetcher);
3978
4051
  return source;
3979
4052
  };
@@ -3981,7 +4054,7 @@ async function handleSAPContext(client, args, cachingLayer) {
3981
4054
  if (type !== 'DDLS') {
3982
4055
  return errorResult('SAPContext(action="impact") supports DDLS only. For non-CDS objects, use SAPNavigate(action="references").');
3983
4056
  }
3984
- const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
4057
+ const ddlSource = await cachedGet('DDLS', name, (ifNoneMatch) => client.getDdls(name, { ifNoneMatch }));
3985
4058
  const upstream = buildCdsUpstream(extractCdsDependencies(ddlSource));
3986
4059
  const includeIndirect = args.includeIndirect === true;
3987
4060
  const siblingCheck = args.siblingCheck !== false;
@@ -4146,24 +4219,24 @@ async function handleSAPContext(client, args, cachingLayer) {
4146
4219
  else {
4147
4220
  switch (type) {
4148
4221
  case 'CLAS':
4149
- source = await cachedGet('CLAS', name, () => client.getClass(name));
4222
+ source = await cachedGet('CLAS', name, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch }));
4150
4223
  break;
4151
4224
  case 'INTF':
4152
- source = await cachedGet('INTF', name, () => client.getInterface(name));
4225
+ source = await cachedGet('INTF', name, (ifNoneMatch) => client.getInterface(name, { ifNoneMatch }));
4153
4226
  break;
4154
4227
  case 'PROG':
4155
- source = await cachedGet('PROG', name, () => client.getProgram(name));
4228
+ source = await cachedGet('PROG', name, (ifNoneMatch) => client.getProgram(name, { ifNoneMatch }));
4156
4229
  break;
4157
4230
  case 'FUNC': {
4158
4231
  const group = String(args.group ?? '');
4159
4232
  if (!group) {
4160
4233
  return errorResult('The "group" parameter is required for FUNC type. Use SAPSearch to find the function group.');
4161
4234
  }
4162
- source = await cachedGet('FUNC', name, () => client.getFunction(group, name));
4235
+ source = await cachedGet('FUNC', name, (ifNoneMatch) => client.getFunction(group, name, { ifNoneMatch }));
4163
4236
  break;
4164
4237
  }
4165
4238
  case 'DDLS': {
4166
- const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
4239
+ const ddlSource = await cachedGet('DDLS', name, (ifNoneMatch) => client.getDdls(name, { ifNoneMatch }));
4167
4240
  const cdsResult = await compressCdsContext(client, ddlSource, name, maxDeps, depth, cachingLayer);
4168
4241
  return textResult(cdsResult.output);
4169
4242
  }
@@ -4536,6 +4609,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
4536
4609
  enabled: true,
4537
4610
  warmupAvailable: cachingLayer.isWarmupAvailable,
4538
4611
  ...stats,
4612
+ inactiveListCache: cachingLayer.inactiveLists.stats(),
4539
4613
  }, null, 2));
4540
4614
  }
4541
4615
  case 'probe': {